}
- suffix={
}
- placeholder={activeFlow?.instruction ?? 'What would you like to do? Try some suggestions…'}
+ suffix={
}
+ placeholder={activeFlow?.instruction ?? 'Run a command…'}
autoFocus
value={input}
onChange={setInput}
diff --git a/frontend/src/lib/components/CommandBar/ActionResult.tsx b/frontend/src/lib/components/CommandBar/ActionResult.tsx
index db1ab9d972dc6..e10144edb6b4f 100644
--- a/frontend/src/lib/components/CommandBar/ActionResult.tsx
+++ b/frontend/src/lib/components/CommandBar/ActionResult.tsx
@@ -22,9 +22,11 @@ export const ActionResult = ({ result, focused }: SearchResultProps): JSX.Elemen
}, [focused])
return (
-
+
{
onMouseEnterResult(result.index)
}}
@@ -42,6 +44,7 @@ export const ActionResult = ({ result, focused }: SearchResultProps): JSX.Elemen
{result.display}
+ {focused &&
Run command
}
)
diff --git a/frontend/src/lib/components/CommandBar/CommandBar.stories.tsx b/frontend/src/lib/components/CommandBar/CommandBar.stories.tsx
new file mode 100644
index 0000000000000..898e3bc1de0c4
--- /dev/null
+++ b/frontend/src/lib/components/CommandBar/CommandBar.stories.tsx
@@ -0,0 +1,289 @@
+import { Meta } from '@storybook/react'
+import { useActions } from 'kea'
+import { commandBarLogic } from 'lib/components/CommandBar/commandBarLogic'
+import { BarStatus } from 'lib/components/CommandBar/types'
+import { useEffect } from 'react'
+
+import { mswDecorator } from '~/mocks/browser'
+
+import { CommandBar } from './CommandBar'
+
+const SEARCH_RESULT = {
+ results: [
+ {
+ type: 'insight',
+ result_id: '3b7NrJXF',
+ extra_fields: {
+ name: '',
+ description: '',
+ derived_name: 'SQL query',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: 'U2W7bAq1',
+ extra_fields: {
+ name: '',
+ description: '',
+ derived_name: 'All events → All events user conversion rate',
+ },
+ },
+ {
+ type: 'feature_flag',
+ result_id: '120',
+ extra_fields: {
+ key: 'person-on-events-enabled',
+ name: 'person-on-events-enabled',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: '44fpCyF7',
+ extra_fields: {
+ name: '',
+ description: '',
+ derived_name: 'User lifecycle based on Pageview',
+ },
+ },
+ {
+ type: 'feature_flag',
+ result_id: '150',
+ extra_fields: {
+ key: 'cs-dashboards',
+ name: 'cs-dashboards',
+ },
+ },
+ {
+ type: 'notebook',
+ result_id: 'b1ZyFO6K',
+ extra_fields: {
+ title: 'Notes 27/09',
+ text_content: 'Notes 27/09\nasd\nas\nda\ns\nd\nlalala',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: 'Ap5YYl2H',
+ extra_fields: {
+ name: '',
+ description: '',
+ derived_name:
+ 'Pageview count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: '4Xaltnro',
+ extra_fields: {
+ name: '',
+ description: '',
+ derived_name: 'User paths based on page views and custom events',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: 'HUkkq7Au',
+ extra_fields: {
+ name: '',
+ description: '',
+ derived_name:
+ 'Pageview count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count & All events count',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: 'hF5z02Iw',
+ extra_fields: {
+ name: '',
+ description: '',
+ derived_name: 'Pageview count & All events count',
+ },
+ },
+ {
+ type: 'feature_flag',
+ result_id: '143',
+ extra_fields: {
+ key: 'high-frequency-batch-exports',
+ name: 'high-frequency-batch-exports',
+ },
+ },
+ {
+ type: 'feature_flag',
+ result_id: '126',
+ extra_fields: {
+ key: 'onboarding-v2-demo',
+ name: 'onboarding-v2-demo',
+ },
+ },
+ {
+ type: 'feature_flag',
+ result_id: '142',
+ extra_fields: {
+ key: 'web-analytics',
+ name: 'web-analytics',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: '94r9bOyB',
+ extra_fields: {
+ name: '',
+ description: '',
+ derived_name: 'Pageview count & All events count',
+ },
+ },
+ {
+ type: 'dashboard',
+ result_id: '1',
+ extra_fields: {
+ name: '🔑 Key metrics',
+ description: 'Company overview.',
+ },
+ },
+ {
+ type: 'notebook',
+ result_id: 'eq4n8PQY',
+ extra_fields: {
+ title: 'asd',
+ text_content: 'asd',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: 'QcCPEk7d',
+ extra_fields: {
+ name: 'Daily unique visitors over time',
+ description: null,
+ derived_name: '$pageview unique users & All events count',
+ },
+ },
+ {
+ type: 'feature_flag',
+ result_id: '133',
+ extra_fields: {
+ key: 'feedback-scene',
+ name: 'feedback-scene',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: 'PWwez0ma',
+ extra_fields: {
+ name: 'Most popular pages',
+ description: null,
+ derived_name: null,
+ },
+ },
+ {
+ type: 'insight',
+ result_id: 'HKTERZ40',
+ extra_fields: {
+ name: 'Feature Flag calls made by unique users per variant',
+ description:
+ 'Shows the number of unique user calls made on feature flag per variant with key: notebooks',
+ derived_name: null,
+ },
+ },
+ {
+ type: 'feature_flag',
+ result_id: '161',
+ extra_fields: {
+ key: 'console-recording-search',
+ name: 'console-recording-search',
+ },
+ },
+ {
+ type: 'feature_flag',
+ result_id: '134',
+ extra_fields: {
+ key: 'early-access-feature',
+ name: 'early-access-feature',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: 'uE7xieYc',
+ extra_fields: {
+ name: '',
+ description: '',
+ derived_name: 'Pageview count',
+ },
+ },
+ {
+ type: 'feature_flag',
+ result_id: '159',
+ extra_fields: {
+ key: 'surveys-multiple-questions',
+ name: 'surveys-multiple-questions',
+ },
+ },
+ {
+ type: 'insight',
+ result_id: 'AVPsaax4',
+ extra_fields: {
+ name: 'Monthly app revenue',
+ description: null,
+ derived_name: null,
+ },
+ },
+ ],
+ counts: {
+ insight: 80,
+ dashboard: 14,
+ experiment: 1,
+ feature_flag: 66,
+ notebook: 2,
+ action: 4,
+ cohort: 3,
+ },
+}
+
+const meta: Meta
= {
+ title: 'Components/Command Bar',
+ component: CommandBar,
+ decorators: [
+ mswDecorator({
+ get: {
+ '/api/projects/:team_id/search': SEARCH_RESULT,
+ },
+ }),
+ ],
+ parameters: {
+ layout: 'fullscreen',
+ testOptions: {
+ snapshotTargetSelector: '[data-attr="command-bar"]',
+ },
+ viewMode: 'story',
+ },
+}
+export default meta
+
+export function Search(): JSX.Element {
+ const { setCommandBar } = useActions(commandBarLogic)
+
+ useEffect(() => {
+ setCommandBar(BarStatus.SHOW_SEARCH)
+ }, [])
+
+ return
+}
+
+export function Actions(): JSX.Element {
+ const { setCommandBar } = useActions(commandBarLogic)
+
+ useEffect(() => {
+ setCommandBar(BarStatus.SHOW_ACTIONS)
+ }, [])
+
+ return
+}
+
+export function Shortcuts(): JSX.Element {
+ const { setCommandBar } = useActions(commandBarLogic)
+
+ useEffect(() => {
+ setCommandBar(BarStatus.SHOW_SHORTCUTS)
+ }, [])
+
+ return
+}
diff --git a/frontend/src/lib/components/CommandBar/CommandBar.tsx b/frontend/src/lib/components/CommandBar/CommandBar.tsx
index dc37957fe6ec8..480cf294d9e3a 100644
--- a/frontend/src/lib/components/CommandBar/CommandBar.tsx
+++ b/frontend/src/lib/components/CommandBar/CommandBar.tsx
@@ -2,27 +2,46 @@ import './index.scss'
import { useActions, useValues } from 'kea'
import { useOutsideClickHandler } from 'lib/hooks/useOutsideClickHandler'
-import { useRef } from 'react'
+import { forwardRef, useRef } from 'react'
import { ActionBar } from './ActionBar'
import { commandBarLogic } from './commandBarLogic'
import { SearchBar } from './SearchBar'
+import { Shortcuts } from './Shortcuts'
import { BarStatus } from './types'
-const CommandBarOverlay = ({ children }: { children?: React.ReactNode }): JSX.Element => (
-
- {children}
-
-)
+interface CommandBarOverlayProps {
+ barStatus: BarStatus
+ children?: React.ReactNode
+}
+
+const CommandBarOverlay = forwardRef(function CommandBarOverlayInternal(
+ { barStatus, children },
+ ref
+): JSX.Element {
+ return (
+
+ )
+})
export function CommandBar(): JSX.Element | null {
const containerRef = useRef(null)
@@ -36,15 +55,10 @@ export function CommandBar(): JSX.Element | null {
}
return (
-
-
- {barStatus === BarStatus.SHOW_SEARCH ?
:
}
-
+
+ {barStatus === BarStatus.SHOW_SEARCH && }
+ {barStatus === BarStatus.SHOW_ACTIONS && }
+ {barStatus === BarStatus.SHOW_SHORTCUTS && }
)
}
diff --git a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx
index 4ccde3c6612cc..e71cda427e5bd 100644
--- a/frontend/src/lib/components/CommandBar/SearchBarTab.tsx
+++ b/frontend/src/lib/components/CommandBar/SearchBarTab.tsx
@@ -17,7 +17,9 @@ export const SearchBarTab = ({ type, active, count, inputRef }: SearchBarTabProp
const { setActiveTab } = useActions(searchBarLogic)
return (
{
setActiveTab(type)
inputRef.current?.focus()
diff --git a/frontend/src/lib/components/CommandBar/SearchInput.tsx b/frontend/src/lib/components/CommandBar/SearchInput.tsx
index 20604ab8c1f89..3d79b64531e78 100644
--- a/frontend/src/lib/components/CommandBar/SearchInput.tsx
+++ b/frontend/src/lib/components/CommandBar/SearchInput.tsx
@@ -1,5 +1,6 @@
import { LemonInput } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
+import { isMac } from 'lib/utils'
import { forwardRef, Ref } from 'react'
import { teamLogic } from 'scenes/teamLogic'
@@ -12,19 +13,23 @@ export const SearchInput = forwardRef(function _SearchInput(_, ref: Ref
}
+ suffix={}
+ placeholder={placeholder}
autoFocus
value={searchQuery}
onChange={setSearchQuery}
- placeholder={currentTeam ? `Search the ${currentTeam.name} project…` : 'Search…'}
/>
)
diff --git a/frontend/src/lib/components/CommandBar/SearchResult.tsx b/frontend/src/lib/components/CommandBar/SearchResult.tsx
index f98395b870a05..9a14a6203fa5a 100644
--- a/frontend/src/lib/components/CommandBar/SearchResult.tsx
+++ b/frontend/src/lib/components/CommandBar/SearchResult.tsx
@@ -1,6 +1,13 @@
import { LemonSkeleton } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { useLayoutEffect, useRef } from 'react'
+import { summarizeInsight } from 'scenes/insights/summarizeInsight'
+import { mathsLogic } from 'scenes/trends/mathsLogic'
+
+import { cohortsModel } from '~/models/cohortsModel'
+import { groupsModel } from '~/models/groupsModel'
+import { Node } from '~/queries/schema'
+import { FilterType } from '~/types'
import { resultTypeToName } from './constants'
import { searchBarLogic, urlForResult } from './searchBarLogic'
@@ -41,9 +48,7 @@ export const SearchResult = ({ result, resultIndex, focused, keyboardFocused }:
return (
{
if (isAutoScrolling) {
return
@@ -88,9 +93,23 @@ type ResultNameProps = {
}
export const ResultName = ({ result }: ResultNameProps): JSX.Element | null => {
+ const { aggregationLabel } = useValues(groupsModel)
+ const { cohortsById } = useValues(cohortsModel)
+ const { mathDefinitions } = useValues(mathsLogic)
+
const { type, extra_fields } = result
if (type === 'insight') {
- return extra_fields.name ?
{extra_fields.name} :
{extra_fields.derived_name}
+ return extra_fields.name ? (
+
{extra_fields.name}
+ ) : (
+
+ {summarizeInsight(extra_fields.query as Node | null, extra_fields.filters as Partial, {
+ aggregationLabel,
+ cohortsById,
+ mathDefinitions,
+ })}
+
+ )
} else if (type === 'feature_flag') {
return
{extra_fields.key}
} else if (type === 'notebook') {
diff --git a/frontend/src/lib/components/CommandBar/SearchTabs.tsx b/frontend/src/lib/components/CommandBar/SearchTabs.tsx
index fe6e9a9edb2ad..4e3d65c29cadd 100644
--- a/frontend/src/lib/components/CommandBar/SearchTabs.tsx
+++ b/frontend/src/lib/components/CommandBar/SearchTabs.tsx
@@ -17,7 +17,7 @@ export const SearchTabs = ({ inputRef }: SearchTabsProps): JSX.Element | null =>
}
return (
-
+
{Object.entries(searchResponse.counts).map(([type, count]) => (
{
+ useMountedLogic(shortcutsLogic)
+
+ return (
+
+
Keyboard shortcuts
+
Site-wide shortcuts
+
+
+ Open search
+
+
+ Open command palette
+
+
+
+ )
+}
diff --git a/frontend/src/lib/components/CommandBar/actionBarLogic.ts b/frontend/src/lib/components/CommandBar/actionBarLogic.ts
index 5341f726d8612..c936929fed9c2 100644
--- a/frontend/src/lib/components/CommandBar/actionBarLogic.ts
+++ b/frontend/src/lib/components/CommandBar/actionBarLogic.ts
@@ -63,7 +63,7 @@ export const actionBarLogic = kea([
// navigate to previous result
event.preventDefault()
actions.onArrowUp()
- } else if (event.key === 'Escape') {
+ } else if (event.key === 'Escape' && event.repeat === false) {
event.preventDefault()
if (values.activeFlow) {
@@ -77,7 +77,7 @@ export const actionBarLogic = kea([
actions.hidePalette()
}
} else if (event.key === 'Backspace') {
- if (values.input.length === 0) {
+ if (values.input.length === 0 && event.repeat === false) {
// transition to search when pressing backspace with empty input
actions.setCommandBar(BarStatus.SHOW_SEARCH)
}
diff --git a/frontend/src/lib/components/CommandBar/commandBarLogic.ts b/frontend/src/lib/components/CommandBar/commandBarLogic.ts
index 4d39cccd297a9..ef6df079ddea1 100644
--- a/frontend/src/lib/components/CommandBar/commandBarLogic.ts
+++ b/frontend/src/lib/components/CommandBar/commandBarLogic.ts
@@ -10,6 +10,7 @@ export const commandBarLogic = kea([
hideCommandBar: true,
toggleSearchBar: true,
toggleActionsBar: true,
+ toggleShortcutOverview: true,
}),
reducers({
barStatus: [
@@ -18,9 +19,15 @@ export const commandBarLogic = kea([
setCommandBar: (_, { status }) => status,
hideCommandBar: () => BarStatus.HIDDEN,
toggleSearchBar: (previousState) =>
- previousState === BarStatus.HIDDEN ? BarStatus.SHOW_SEARCH : BarStatus.HIDDEN,
+ [BarStatus.HIDDEN, BarStatus.SHOW_SHORTCUTS].includes(previousState)
+ ? BarStatus.SHOW_SEARCH
+ : BarStatus.HIDDEN,
toggleActionsBar: (previousState) =>
- previousState === BarStatus.HIDDEN ? BarStatus.SHOW_ACTIONS : BarStatus.HIDDEN,
+ [BarStatus.HIDDEN, BarStatus.SHOW_SHORTCUTS].includes(previousState)
+ ? BarStatus.SHOW_ACTIONS
+ : BarStatus.HIDDEN,
+ toggleShortcutOverview: (previousState) =>
+ previousState === BarStatus.HIDDEN ? BarStatus.SHOW_SHORTCUTS : previousState,
},
],
}),
@@ -36,6 +43,8 @@ export const commandBarLogic = kea([
// cmd+k opens search
actions.toggleSearchBar()
}
+ } else if (event.shiftKey && event.key === '?') {
+ actions.toggleShortcutOverview()
}
}
window.addEventListener('keydown', cache.onKeyDown)
diff --git a/frontend/src/lib/components/CommandBar/index.scss b/frontend/src/lib/components/CommandBar/index.scss
index 80621cf83d7c9..c8a200a7f5740 100644
--- a/frontend/src/lib/components/CommandBar/index.scss
+++ b/frontend/src/lib/components/CommandBar/index.scss
@@ -1,4 +1,17 @@
.CommandBar__input {
border-color: transparent !important;
border-radius: 0;
+ height: 2.75rem;
+ padding-left: 0.75rem;
+ padding-right: 0.375rem;
+}
+
+.SearchBarTab {
+ &:hover {
+ border-top: 2px solid var(--border-3000);
+ }
+
+ &.SearchBarTab__active {
+ border-color: var(--primary-3000);
+ }
}
diff --git a/frontend/src/lib/components/CommandBar/searchBarLogic.ts b/frontend/src/lib/components/CommandBar/searchBarLogic.ts
index e970a23e2a543..2f3b04715c598 100644
--- a/frontend/src/lib/components/CommandBar/searchBarLogic.ts
+++ b/frontend/src/lib/components/CommandBar/searchBarLogic.ts
@@ -90,6 +90,10 @@ export const searchBarLogic = kea([
(s) => [s.keyboardResultIndex, s.hoverResultIndex],
(keyboardResultIndex: number, hoverResultIndex: number | null) => hoverResultIndex || keyboardResultIndex,
],
+ tabs: [
+ (s) => [s.searchCounts],
+ (counts): ResultTypeWithAll[] => ['all', ...(Object.keys(counts || {}) as ResultTypeWithAll[])],
+ ],
}),
listeners(({ values, actions }) => ({
openResult: ({ index }) => {
@@ -118,7 +122,7 @@ export const searchBarLogic = kea([
// navigate to previous result
event.preventDefault()
actions.onArrowUp(values.activeResultIndex, values.maxIndex)
- } else if (event.key === 'Escape') {
+ } else if (event.key === 'Escape' && event.repeat === false) {
// hide command bar
actions.hideCommandBar()
} else if (event.key === '>') {
@@ -133,6 +137,16 @@ export const searchBarLogic = kea([
event.preventDefault()
actions.setCommandBar(BarStatus.SHOW_ACTIONS)
}
+ } else if (event.key === 'Tab') {
+ event.preventDefault()
+ const currentIndex = values.tabs.findIndex((tab) => tab === values.activeTab)
+ if (event.shiftKey) {
+ const prevIndex = currentIndex === 0 ? values.tabs.length - 1 : currentIndex - 1
+ actions.setActiveTab(values.tabs[prevIndex])
+ } else {
+ const nextIndex = currentIndex === values.tabs.length - 1 ? 0 : currentIndex + 1
+ actions.setActiveTab(values.tabs[nextIndex])
+ }
}
}
window.addEventListener('keydown', cache.onKeyDown)
diff --git a/frontend/src/lib/components/CommandBar/shortcutsLogic.ts b/frontend/src/lib/components/CommandBar/shortcutsLogic.ts
new file mode 100644
index 0000000000000..4e70c5920f41c
--- /dev/null
+++ b/frontend/src/lib/components/CommandBar/shortcutsLogic.ts
@@ -0,0 +1,25 @@
+import { afterMount, beforeUnmount, connect, kea, path } from 'kea'
+
+import { commandBarLogic } from './commandBarLogic'
+import type { shortcutsLogicType } from './shortcutsLogicType'
+
+export const shortcutsLogic = kea([
+ path(['lib', 'components', 'CommandBar', 'shortcutsLogic']),
+ connect({
+ actions: [commandBarLogic, ['hideCommandBar']],
+ }),
+ afterMount(({ actions, cache }) => {
+ // register keyboard shortcuts
+ cache.onKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ // hide command bar
+ actions.hideCommandBar()
+ }
+ }
+ window.addEventListener('keydown', cache.onKeyDown)
+ }),
+ beforeUnmount(({ cache }) => {
+ // unregister keyboard shortcuts
+ window.removeEventListener('keydown', cache.onKeyDown)
+ }),
+])
diff --git a/frontend/src/lib/components/CommandBar/types.ts b/frontend/src/lib/components/CommandBar/types.ts
index 1f3278f3727f6..3a6a482c26453 100644
--- a/frontend/src/lib/components/CommandBar/types.ts
+++ b/frontend/src/lib/components/CommandBar/types.ts
@@ -2,6 +2,7 @@ export enum BarStatus {
HIDDEN = 'hidden',
SHOW_SEARCH = 'show_search',
SHOW_ACTIONS = 'show_actions',
+ SHOW_SHORTCUTS = 'show_shortcuts',
}
export type ResultType = 'action' | 'cohort' | 'insight' | 'dashboard' | 'experiment' | 'feature_flag' | 'notebook'
diff --git a/frontend/src/lib/components/CommandPalette/CommandPalette.scss b/frontend/src/lib/components/CommandPalette/CommandPalette.scss
index e2622169149a0..55079ad3ac496 100644
--- a/frontend/src/lib/components/CommandPalette/CommandPalette.scss
+++ b/frontend/src/lib/components/CommandPalette/CommandPalette.scss
@@ -121,5 +121,5 @@
.palette__icon {
display: flex;
align-items: center;
- font-size: 1rem;
+ font-size: 1.25rem;
}
diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx
index 380902a2d520b..8dedc08691066 100644
--- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx
+++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx
@@ -1,50 +1,64 @@
-import { Parser } from 'expr-eval'
-import Fuse from 'fuse.js'
-import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea'
-import { router } from 'kea-router'
-import api from 'lib/api'
-import { FEATURE_FLAGS } from 'lib/constants'
import {
- IconAction,
IconApps,
- IconBarChart,
- IconCalculate,
- IconCheckmark,
- IconCohort,
- IconComment,
- IconCorporate,
- IconCottage,
- IconEmojiPeople,
- IconFlag,
- IconFunnelHorizontal,
- IconGauge,
+ IconAsterisk,
+ IconCalculator,
+ IconChat,
+ IconCheck,
+ IconCursor,
+ IconDashboard,
+ IconDatabase,
+ IconDay,
+ IconExternal,
+ IconFunnels,
+ IconGear,
IconGithub,
+ IconGraph,
+ IconHogQL,
+ IconHome,
IconKeyboard,
+ IconLeave,
+ IconLifecycle,
+ IconList,
IconLive,
- IconLockOpen,
- IconLogout,
- IconOpenInNew,
- IconPerson,
- IconPersonFilled,
- IconRecording,
+ IconNight,
+ IconNotebook,
+ IconPageChart,
+ IconPeople,
+ IconPeopleFilled,
+ IconPieChart,
+ IconRetention,
+ IconRewindPlay,
+ IconRocket,
IconServer,
- IconSettings,
- IconTableChart,
- IconTools,
- IconTrendingFlat,
- IconTrendingUp,
-} from 'lib/lemon-ui/icons'
+ IconStickiness,
+ IconTestTube,
+ IconThoughtBubble,
+ IconToggle,
+ IconToolbar,
+ IconTrends,
+ IconUnlock,
+ IconUserPaths,
+} from '@posthog/icons'
+import { Parser } from 'expr-eval'
+import Fuse from 'fuse.js'
+import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea'
+import { router } from 'kea-router'
+import api from 'lib/api'
+import { FEATURE_FLAGS } from 'lib/constants'
import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
-import { isMobile, isURL, sample, uniqueBy } from 'lib/utils'
+import { isMobile, isURL, uniqueBy } from 'lib/utils'
import { copyToClipboard } from 'lib/utils/copyToClipboard'
import posthog from 'posthog-js'
import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic'
+import { insightTypeURL } from 'scenes/insights/utils'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'
+import { ThemeIcon } from '~/layout/navigation-3000/components/Navbar'
+import { themeLogic } from '~/layout/navigation-3000/themeLogic'
import { dashboardsModel } from '~/models/dashboardsModel'
import { DashboardType, InsightType } from '~/types'
@@ -120,7 +134,7 @@ function resolveCommand(source: Command | CommandFlow, argument?: string, prefix
export const commandPaletteLogic = kea([
path(['lib', 'components', 'CommandPalette', 'commandPaletteLogic']),
connect({
- actions: [personalAPIKeysLogic, ['createKey'], router, ['push']],
+ actions: [personalAPIKeysLogic, ['createKey'], router, ['push'], themeLogic, ['overrideTheme']],
values: [teamLogic, ['currentTeam'], userLogic, ['user'], featureFlagLogic, ['featureFlags']],
logic: [preflightLogic],
}),
@@ -242,7 +256,7 @@ export const commandPaletteLogic = kea([
key: 'custom_dashboards',
resolver: dashboards.map((dashboard: DashboardType) => ({
key: `dashboard_${dashboard.id}`,
- icon: IconTableChart,
+ icon: IconPageChart,
display: `Go to dashboard: ${dashboard.name}`,
executor: () => {
const { push } = router.actions
@@ -319,7 +333,7 @@ export const commandPaletteLogic = kea([
.search(argument)
.slice(0, RESULTS_MAX)
.map((result) => result.item)
- : sample(fusableResults, RESULTS_MAX - guaranteedResults.length)
+ : fusableResults.slice(0, RESULTS_MAX)
return guaranteedResults.concat(fusedResults)
},
],
@@ -397,7 +411,7 @@ export const commandPaletteLogic = kea([
key: `person-${person.distinct_ids[0]}`,
resolver: [
{
- icon: IconPersonFilled,
+ icon: IconPeopleFilled,
display: `View person ${input}`,
executor: () => {
const { push } = router.actions
@@ -421,67 +435,128 @@ export const commandPaletteLogic = kea([
prefixes: ['open', 'visit'],
resolver: [
{
- icon: IconGauge,
+ icon: IconDashboard,
display: 'Go to Dashboards',
executor: () => {
push(urls.dashboards())
},
},
{
- icon: IconBarChart,
+ icon: IconHome,
+ display: 'Go to Project homepage',
+ executor: () => {
+ push(urls.projectHomepage())
+ },
+ },
+ {
+ icon: IconGraph,
display: 'Go to Insights',
executor: () => {
push(urls.savedInsights())
},
},
{
- icon: IconTrendingUp,
- display: 'Go to Trends',
+ icon: IconTrends,
+ display: 'Create a new Trend insight',
executor: () => {
// TODO: Don't reset insight on change
push(urls.insightNew({ insight: InsightType.TRENDS }))
},
},
{
- icon: IconFunnelHorizontal,
- display: 'Go to Funnels',
+ icon: IconFunnels,
+ display: 'Create a new Funnel insight',
executor: () => {
// TODO: Don't reset insight on change
push(urls.insightNew({ insight: InsightType.FUNNELS }))
},
},
{
- icon: IconTrendingFlat,
- display: 'Go to Retention',
+ icon: IconRetention,
+ display: 'Create a new Retention insight',
executor: () => {
// TODO: Don't reset insight on change
push(urls.insightNew({ insight: InsightType.RETENTION }))
},
},
{
- icon: IconEmojiPeople,
- display: 'Go to Paths',
+ icon: IconUserPaths,
+ display: 'Create a new Paths insight',
executor: () => {
// TODO: Don't reset insight on change
push(urls.insightNew({ insight: InsightType.PATHS }))
},
},
+ {
+ icon: IconStickiness,
+ display: 'Create a new Stickiness insight',
+ executor: () => {
+ // TODO: Don't reset insight on change
+ push(urls.insightNew({ insight: InsightType.STICKINESS }))
+ },
+ },
+ {
+ icon: IconLifecycle,
+ display: 'Create a new Lifecycle insight',
+ executor: () => {
+ // TODO: Don't reset insight on change
+ push(urls.insightNew({ insight: InsightType.LIFECYCLE }))
+ },
+ },
+ {
+ icon: IconHogQL,
+ display: 'Create a new HogQL insight',
+ synonyms: ['hogql', 'sql'],
+ executor: () => {
+ // TODO: Don't reset insight on change
+ push(insightTypeURL[InsightType.SQL])
+ },
+ },
+ {
+ icon: IconNotebook,
+ display: 'Go to Notebooks',
+ executor: () => {
+ push(urls.notebooks())
+ },
+ },
{
icon: IconLive,
- display: 'Go to Events',
+ display: 'Go to Events explorer',
executor: () => {
push(urls.events())
},
},
{
- icon: IconAction,
+ icon: IconDatabase,
+ display: 'Go to Data management',
+ synonyms: ['events'],
+ executor: () => {
+ push(urls.eventDefinitions())
+ },
+ },
+ {
+ icon: IconCursor,
display: 'Go to Actions',
executor: () => {
push(urls.actions())
},
},
{
- icon: IconPerson,
+ icon: IconList,
+ display: 'Go to Properties',
+ executor: () => {
+ push(urls.propertyDefinitions())
+ },
+ },
+ {
+ icon: IconThoughtBubble,
+ display: 'Go to Annotations',
+ executor: () => {
+ push(urls.annotations())
+ },
+ },
+ {
+ icon: IconPeople,
display: 'Go to Persons',
synonyms: ['people'],
executor: () => {
@@ -489,77 +564,110 @@ export const commandPaletteLogic = kea([
},
},
{
- icon: IconCohort,
+ icon: IconPeople,
display: 'Go to Cohorts',
executor: () => {
push(urls.cohorts())
},
},
+ ...(values.featureFlags[FEATURE_FLAGS.WEB_ANALYTICS]
+ ? [
+ {
+ icon: IconPieChart,
+ display: 'Go to Web analytics',
+ executor: () => {
+ push(urls.webAnalytics())
+ },
+ },
+ ]
+ : []),
+ ...(values.featureFlags[FEATURE_FLAGS.DATA_WAREHOUSE]
+ ? [
+ {
+ icon: IconServer,
+ display: 'Go to Data warehouse',
+ executor: () => {
+ push(urls.dataWarehouse())
+ },
+ },
+ ]
+ : []),
{
- icon: IconFlag,
- display: 'Go to Feature Flags',
- synonyms: ['feature flags', 'a/b tests'],
+ display: 'Go to Session replay',
+ icon: IconRewindPlay,
+ executor: () => {
+ push(urls.replay())
+ },
+ },
+ {
+ display: 'Go to Surveys',
+ icon: IconChat,
+ executor: () => {
+ push(urls.surveys())
+ },
+ },
+ {
+ icon: IconToggle,
+ display: 'Go to Feature flags',
executor: () => {
push(urls.featureFlags())
},
},
{
- icon: IconComment,
- display: 'Go to Annotations',
+ icon: IconTestTube,
+ display: 'Go to A/B testing',
executor: () => {
- push(urls.annotations())
+ push(urls.experiments())
},
},
{
- icon: IconCorporate,
- display: 'Go to Team members',
- synonyms: ['organization', 'members', 'invites', 'teammates'],
+ icon: IconRocket,
+ display: 'Go to Early access features',
executor: () => {
- push(urls.settings('organization'))
+ push(urls.earlyAccessFeatures())
},
},
{
- icon: IconCottage,
- display: 'Go to project homepage',
+ icon: IconApps,
+ display: 'Go to Apps',
+ synonyms: ['integrations'],
executor: () => {
- push(urls.projectHomepage())
+ push(urls.projectApps())
},
},
{
- icon: IconSettings,
- display: 'Go to Project settings',
+ icon: IconToolbar,
+ display: 'Go to Toolbar',
executor: () => {
- push(urls.settings('project'))
+ push(urls.toolbarLaunch())
},
},
{
- icon: () => (
-
- ),
- display: 'Go to My settings',
- synonyms: ['account'],
+ icon: IconGear,
+ display: 'Go to Project settings',
executor: () => {
- push(urls.settings('user'))
+ push(urls.settings('project'))
},
},
{
- icon: IconApps,
- display: 'Go to Apps',
- synonyms: ['integrations'],
+ icon: IconGear,
+ display: 'Go to Organization settings',
executor: () => {
- push(urls.projectApps())
+ push(urls.settings('organization'))
},
},
{
- icon: IconServer,
- display: 'Go to Instance status & settings',
- synonyms: ['redis', 'celery', 'django', 'postgres', 'backend', 'service', 'online'],
+ icon: () => (
+
+ ),
+ display: 'Go to User settings',
+ synonyms: ['account', 'profile'],
executor: () => {
- push(urls.instanceStatus())
+ push(urls.settings('user'))
},
},
{
- icon: IconLogout,
+ icon: IconLeave,
display: 'Log out',
executor: () => {
userLogic.actions.logout()
@@ -577,7 +685,7 @@ export const commandPaletteLogic = kea([
preflightLogic.values.preflight?.is_debug ||
preflightLogic.values.preflight?.instance_preferences?.debug_queries
? {
- icon: IconTools,
+ icon: IconDatabase,
display: 'Debug ClickHouse Queries',
executor: () => openCHQueriesDebugModal(),
}
@@ -588,7 +696,7 @@ export const commandPaletteLogic = kea([
key: 'debug-copy-session-recording-url',
scope: GLOBAL_COMMAND_SCOPE,
resolver: {
- icon: IconRecording,
+ icon: IconRewindPlay,
display: 'Debug: Copy the session recording link to clipboard',
executor: () => {
const url = posthog.get_session_replay_url({ withTimestamp: true, timestampLookBack: 30 })
@@ -610,7 +718,7 @@ export const commandPaletteLogic = kea([
return isNaN(result)
? null
: {
- icon: IconCalculate,
+ icon: IconCalculator,
display: `= ${result}`,
guarantee: true,
executor: () => {
@@ -630,7 +738,7 @@ export const commandPaletteLogic = kea([
resolver: (argument) => {
const results: CommandResultTemplate[] = (teamLogic.values.currentTeam?.app_urls ?? []).map(
(url: string) => ({
- icon: IconOpenInNew,
+ icon: IconExternal,
display: `Open ${url}`,
synonyms: [`Visit ${url}`],
executor: () => {
@@ -640,7 +748,7 @@ export const commandPaletteLogic = kea([
)
if (argument && isURL(argument)) {
results.push({
- icon: IconOpenInNew,
+ icon: IconExternal,
display: `Open ${argument}`,
synonyms: [`Visit ${argument}`],
executor: () => {
@@ -649,7 +757,7 @@ export const commandPaletteLogic = kea([
})
}
results.push({
- icon: IconOpenInNew,
+ icon: IconExternal,
display: 'Open PostHog Docs',
synonyms: ['technical documentation'],
executor: () => {
@@ -664,7 +772,7 @@ export const commandPaletteLogic = kea([
key: 'create-personal-api-key',
scope: GLOBAL_COMMAND_SCOPE,
resolver: {
- icon: IconLockOpen,
+ icon: IconUnlock,
display: 'Create Personal API Key',
executor: () => ({
instruction: 'Give your key a label',
@@ -673,7 +781,7 @@ export const commandPaletteLogic = kea([
resolver: (argument) => {
if (argument?.length) {
return {
- icon: IconLockOpen,
+ icon: IconUnlock,
display: `Create Key "${argument}"`,
executor: () => {
personalAPIKeysLogic.actions.createKey(argument)
@@ -691,7 +799,7 @@ export const commandPaletteLogic = kea([
key: 'create-dashboard',
scope: GLOBAL_COMMAND_SCOPE,
resolver: {
- icon: IconGauge,
+ icon: IconDashboard,
display: 'Create Dashboard',
executor: () => ({
instruction: 'Name your new dashboard',
@@ -700,7 +808,7 @@ export const commandPaletteLogic = kea([
resolver: (argument) => {
if (argument?.length) {
return {
- icon: IconGauge,
+ icon: IconDashboard,
display: `Create Dashboard "${argument}"`,
executor: () => {
newDashboardLogic.actions.addDashboard({ name: argument })
@@ -717,7 +825,7 @@ export const commandPaletteLogic = kea([
key: 'share-feedback',
scope: GLOBAL_COMMAND_SCOPE,
resolver: {
- icon: IconComment,
+ icon: IconThoughtBubble,
display: 'Share Feedback',
synonyms: ['send opinion', 'ask question', 'message posthog', 'github issue'],
executor: () => ({
@@ -725,12 +833,12 @@ export const commandPaletteLogic = kea([
resolver: [
{
display: 'Send Message Directly to PostHog',
- icon: IconComment,
+ icon: IconThoughtBubble,
executor: () => ({
instruction: "What's on your mind?",
- icon: IconComment,
+ icon: IconThoughtBubble,
resolver: (argument) => ({
- icon: IconComment,
+ icon: IconThoughtBubble,
display: 'Send',
executor: !argument?.length
? undefined
@@ -738,7 +846,7 @@ export const commandPaletteLogic = kea([
posthog.capture('palette feedback', { message: argument })
return {
resolver: {
- icon: IconCheckmark,
+ icon: IconCheck,
display: 'Message Sent!',
executor: true,
},
@@ -759,6 +867,42 @@ export const commandPaletteLogic = kea([
},
}
+ const toggleTheme: Command = {
+ key: 'toggle-theme',
+ scope: GLOBAL_COMMAND_SCOPE,
+ resolver: {
+ icon: ThemeIcon,
+ display: 'Switch theme',
+ synonyms: ['toggle theme', 'dark mode', 'light mode'],
+ executor: () => ({
+ scope: 'Switch theme',
+ resolver: [
+ {
+ icon: IconDay,
+ display: 'Light theme',
+ executor: () => {
+ actions.overrideTheme(false)
+ },
+ },
+ {
+ icon: IconNight,
+ display: 'Dark theme',
+ executor: () => {
+ actions.overrideTheme(true)
+ },
+ },
+ {
+ icon: IconAsterisk,
+ display: 'Sync with system settings',
+ executor: () => {
+ actions.overrideTheme(null)
+ },
+ },
+ ],
+ }),
+ },
+ }
+
actions.registerCommand(goTo)
actions.registerCommand(openUrls)
actions.registerCommand(debugClickhouseQueries)
@@ -767,6 +911,9 @@ export const commandPaletteLogic = kea([
actions.registerCommand(createDashboard)
actions.registerCommand(shareFeedback)
actions.registerCommand(debugCopySessionRecordingURL)
+ if (values.featureFlags[FEATURE_FLAGS.POSTHOG_3000]) {
+ actions.registerCommand(toggleTheme)
+ }
},
beforeUnmount: () => {
actions.deregisterCommand('go-to')
@@ -777,6 +924,7 @@ export const commandPaletteLogic = kea([
actions.deregisterCommand('create-dashboard')
actions.deregisterCommand('share-feedback')
actions.deregisterCommand('debug-copy-session-recording-url')
+ actions.deregisterCommand('toggle-theme')
},
})),
])
diff --git a/frontend/src/lib/components/CompactList/CompactList.scss b/frontend/src/lib/components/CompactList/CompactList.scss
index cd329a1d8e4f7..930ea6f17b1b1 100644
--- a/frontend/src/lib/components/CompactList/CompactList.scss
+++ b/frontend/src/lib/components/CompactList/CompactList.scss
@@ -31,4 +31,14 @@
overflow: auto auto;
padding: 0 0.5rem 0.5rem;
}
+
+ .LemonButton {
+ font-family: var(--font-sans) !important;
+ }
+
+ .secondary-text {
+ .posthog-3000 & {
+ color: var(--text-secondary);
+ }
+ }
}
diff --git a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.scss b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.scss
index 46fc5d0773ed8..4f9297fe9261b 100644
--- a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.scss
+++ b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.scss
@@ -146,15 +146,6 @@
.definition-popover-edit-form-value {
margin-bottom: 1rem;
-
- &.definition-popover-owner-select {
- .ant-select-selector {
- .ant-select-selection-placeholder {
- color: black;
- font-weight: normal;
- }
- }
- }
}
}
}
diff --git a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx
index 7aa36f83d68ed..db1cdb0b48cdc 100644
--- a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx
+++ b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx
@@ -1,6 +1,6 @@
import './DefinitionPopover.scss'
-import { Divider, DividerProps, Select } from 'antd'
+import { Divider, DividerProps } from 'antd'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { definitionPopoverLogic, DefinitionPopoverState } from 'lib/components/DefinitionPopover/definitionPopoverLogic'
@@ -12,9 +12,8 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { getKeyMapping } from 'lib/taxonomy'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { Owner } from 'scenes/events/Owner'
-import { membersLogic } from 'scenes/organization/membersLogic'
-import { KeyMapping, PropertyDefinition, UserBasicType } from '~/types'
+import { KeyMapping, UserBasicType } from '~/types'
interface DefinitionPopoverProps {
children: React.ReactNode
@@ -238,48 +237,6 @@ function Card({
)
}
-function Type({ propertyType }: { propertyType: PropertyDefinition['property_type'] | null }): JSX.Element {
- return propertyType ? (
-
- ) : (
- <>>
- )
-}
-
-function OwnerDropdown(): JSX.Element {
- const { members } = useValues(membersLogic)
- const { localDefinition } = useValues(definitionPopoverLogic)
- const { setLocalDefinition } = useActions(definitionPopoverLogic)
-
- return (
- }
- style={{ minWidth: 200 }}
- dropdownClassName="owner-option"
- onChange={(val) => {
- const newOwner = members.find((mem) => mem.user.id === val)?.user
- if (newOwner) {
- setLocalDefinition({ owner: newOwner })
- } else {
- setLocalDefinition({ owner: null })
- }
- }}
- >
-
-
-
- {members.map((member) => (
-
-
-
- ))}
-
- )
-}
-
export const DefinitionPopover = {
Wrapper,
Header,
@@ -291,6 +248,4 @@ export const DefinitionPopover = {
Grid,
Section,
Card,
- OwnerDropdown,
- Type,
}
diff --git a/frontend/src/lib/components/PageHeader.tsx b/frontend/src/lib/components/PageHeader.tsx
index 5d2ae29fbfe04..bf9215aac778f 100644
--- a/frontend/src/lib/components/PageHeader.tsx
+++ b/frontend/src/lib/components/PageHeader.tsx
@@ -1,7 +1,9 @@
import clsx from 'clsx'
import { useValues } from 'kea'
+import { FEATURE_FLAGS } from 'lib/constants'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
+import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { createPortal } from 'react-dom'
import { DraggableToNotebook, DraggableToNotebookProps } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook'
@@ -30,6 +32,9 @@ export function PageHeader({
}: PageHeaderProps): JSX.Element | null {
const is3000 = useFeatureFlag('POSTHOG_3000')
const { actionsContainer } = useValues(breadcrumbsLogic)
+ const { featureFlags } = useValues(featureFlagLogic)
+
+ const has3000 = featureFlags[FEATURE_FLAGS.POSTHOG_3000]
return (
<>
@@ -53,7 +58,7 @@ export function PageHeader({
{is3000 && buttons && actionsContainer && createPortal(buttons, actionsContainer)}
{caption && {caption}
}
- {delimited && }
+ {delimited && }
>
)
}
diff --git a/frontend/src/lib/components/VersionChecker/VersionCheckerBanner.tsx b/frontend/src/lib/components/VersionChecker/VersionCheckerBanner.tsx
index 1975b4c90dde7..38094c08dd21f 100644
--- a/frontend/src/lib/components/VersionChecker/VersionCheckerBanner.tsx
+++ b/frontend/src/lib/components/VersionChecker/VersionCheckerBanner.tsx
@@ -3,11 +3,15 @@ import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { versionCheckerLogic } from './versionCheckerLogic'
-export function VersionCheckerBanner(): JSX.Element {
+export function VersionCheckerBanner({ minVersionAccepted }: { minVersionAccepted?: string }): JSX.Element {
const { versionWarning } = useValues(versionCheckerLogic)
-
// We don't want to show a message if the diff is too small (we might be still deploying the changes out)
- if (!versionWarning || versionWarning.diff < 5) {
+ if (
+ !versionWarning ||
+ (minVersionAccepted && versionWarning.currentVersion
+ ? versionWarning.currentVersion.localeCompare(minVersionAccepted) >= 0
+ : versionWarning.diff < 5)
+ ) {
return <>>
}
diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx
index 712c7c77fa957..69c5a65aaa2f8 100644
--- a/frontend/src/lib/constants.tsx
+++ b/frontend/src/lib/constants.tsx
@@ -176,6 +176,7 @@ export const FEATURE_FLAGS = {
PERSON_FEED_CANVAS: 'person-feed-canvas', // owner: #project-canvas
MULTI_PROJECT_FEATURE_FLAGS: 'multi-project-feature-flags', // owner: @jurajmajerik #team-feature-success
NETWORK_PAYLOAD_CAPTURE: 'network-payload-capture', // owner: #team-monitoring
+ FEATURE_FLAG_COHORT_CREATION: 'feature-flag-cohort-creation', // owner: @neilkakkar #team-feature-success
INSIGHT_HORIZONTAL_CONTROLS: 'insight-horizontal-controls', // owner: @benjackwhite
} as const
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]
diff --git a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.scss b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.scss
index e165aaa435d4f..9a948c4f24dd3 100644
--- a/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.scss
+++ b/frontend/src/lib/lemon-ui/LemonBanner/LemonBanner.scss
@@ -1,13 +1,14 @@
.LemonBanner {
+ align-items: center;
border-radius: var(--radius);
- padding: 0.5rem 0.75rem;
+ border: solid 1px var(--border-3000);
color: var(--primary-alt);
- font-weight: 500;
display: flex;
- align-items: center;
- text-align: left;
+ font-weight: 500;
gap: 0.5rem;
min-height: 3rem;
+ padding: 0.5rem 0.75rem;
+ text-align: left;
&.LemonBanner--info {
background-color: var(--primary-alt-highlight);
diff --git a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.scss b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.scss
index 7190ef5c1b0d4..5ed9970665fda 100644
--- a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.scss
+++ b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.scss
@@ -1,24 +1,33 @@
.LemonButton {
- position: relative;
- transition: background-color 200ms ease, color 200ms ease, border 200ms ease, opacity 200ms ease,
- transform 100ms ease;
- display: flex;
- flex-direction: row;
- flex-shrink: 0;
align-items: center;
- justify-content: flex-start;
- padding: 0.25rem 0.75rem;
- gap: 0.5rem;
+ appearance: none !important; // Important as this gets overridden by Ant styles...
background: none;
border-radius: var(--radius);
border: none;
+ cursor: pointer;
+ display: flex;
+ flex-direction: row;
+
+ .posthog-3000 & {
+ font-family: var(--font-title);
+ }
+
+ flex-shrink: 0;
font-size: 0.875rem;
- text-align: left;
- line-height: 1.5rem;
font-weight: 500;
- cursor: pointer;
+ gap: 0.5rem;
+ justify-content: flex-start;
+ line-height: 1.5rem;
+ padding: 0.25rem 0.75rem;
+ position: relative;
+ text-align: left;
+ transition: background-color 200ms ease, color 200ms ease, border 200ms ease, opacity 200ms ease,
+ transform 100ms ease;
user-select: none;
- appearance: none !important; // Important as this gets overridden by Ant styles...
+
+ .font-normal {
+ font-family: var(--font-sans);
+ }
> span {
display: flex;
diff --git a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss
index d92e8fe712bb8..c785fbc3c53b6 100644
--- a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss
+++ b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.scss
@@ -51,8 +51,8 @@
}
.posthog-3000 & {
- --lemon-switch-width: 1.5rem;
- --lemon-switch-height: 0.75rem;
+ --lemon-switch-height: 1.125rem;
+ --lemon-switch-width: calc(11 / 6 * var(--lemon-switch-height)); // Same proportion as in IconToggle
}
}
@@ -117,7 +117,7 @@
justify-content: center;
.posthog-3000 & {
- --lemon-switch-handle-ratio: calc(8 / 10);
+ --lemon-switch-handle-ratio: calc(3 / 4); // Same proportion as in IconToggle
--lemon-switch-handle-gutter: calc(var(--lemon-switch-height) * calc(1 - var(--lemon-switch-handle-ratio)) / 2);
--lemon-switch-handle-width: calc(var(--lemon-switch-height) * var(--lemon-switch-handle-ratio));
--lemon-switch-active-translate: translateX(
diff --git a/frontend/src/lib/utils/eventUsageLogic.ts b/frontend/src/lib/utils/eventUsageLogic.ts
index 560ef2c3d2792..0c1ec31f290ec 100644
--- a/frontend/src/lib/utils/eventUsageLogic.ts
+++ b/frontend/src/lib/utils/eventUsageLogic.ts
@@ -1103,6 +1103,7 @@ export const eventUsageLogic = kea([
posthog.capture('survey created', {
name: survey.name,
id: survey.id,
+ survey_type: survey.type,
questions_length: survey.questions.length,
question_types: survey.questions.map((question) => question.type),
})
@@ -1111,6 +1112,7 @@ export const eventUsageLogic = kea([
posthog.capture('survey launched', {
name: survey.name,
id: survey.id,
+ survey_type: survey.type,
question_types: survey.questions.map((question) => question.type),
created_at: survey.created_at,
start_date: survey.start_date,
diff --git a/frontend/src/loadPostHogJS.tsx b/frontend/src/loadPostHogJS.tsx
index 1a5b4cb7473f4..807fce2883849 100644
--- a/frontend/src/loadPostHogJS.tsx
+++ b/frontend/src/loadPostHogJS.tsx
@@ -27,8 +27,8 @@ export function loadPostHogJS(): void {
bootstrap: window.POSTHOG_USER_IDENTITY_WITH_FLAGS ? window.POSTHOG_USER_IDENTITY_WITH_FLAGS : {},
opt_in_site_apps: true,
loaded: (posthog) => {
- if (posthog.webPerformance) {
- posthog.webPerformance._forceAllowLocalhost = true
+ if (posthog.sessionRecording) {
+ posthog.sessionRecording._forceAllowLocalhostNetworkCapture = true
}
if (window.IMPERSONATED_SESSION) {
diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss
index 0c2b080492e42..6637f4f265e04 100644
--- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss
+++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.scss
@@ -1,6 +1,11 @@
.PropertyGroupFilters {
.property-group {
background-color: var(--side);
+
+ .posthog-3000 & {
+ border-width: 1px;
+ }
+
padding: 0.5rem;
border-radius: 4px;
}
diff --git a/frontend/src/scenes/billing/BillingLimitInput.tsx b/frontend/src/scenes/billing/BillingLimitInput.tsx
index 19299022bc464..db519a096265a 100644
--- a/frontend/src/scenes/billing/BillingLimitInput.tsx
+++ b/frontend/src/scenes/billing/BillingLimitInput.tsx
@@ -3,6 +3,7 @@ import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { LemonDialog } from 'lib/lemon-ui/LemonDialog'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
+import { useRef } from 'react'
import { BillingProductV2AddonType, BillingProductV2Type, BillingV2TierType } from '~/types'
@@ -11,10 +12,11 @@ import { billingLogic } from './billingLogic'
import { billingProductLogic } from './billingProductLogic'
export const BillingLimitInput = ({ product }: { product: BillingProductV2Type }): JSX.Element | null => {
+ const limitInputRef = useRef(null)
const { billing, billingLoading } = useValues(billingLogic)
const { updateBillingLimits } = useActions(billingLogic)
const { isEditingBillingLimit, showBillingLimitInput, billingLimitInput, customLimitUsd } = useValues(
- billingProductLogic({ product })
+ billingProductLogic({ product, billingLimitInputRef: limitInputRef })
)
const { setIsEditingBillingLimit, setBillingLimitInput } = useActions(billingProductLogic({ product }))
@@ -80,7 +82,7 @@ export const BillingLimitInput = ({ product }: { product: BillingProductV2Type }
return null
}
return (
-
+
{!isEditingBillingLimit ? (
@@ -106,6 +108,7 @@ export const BillingLimitInput = ({ product }: { product: BillingProductV2Type }
<>
): BillingV2Type => {
@@ -55,6 +58,8 @@ const parseBillingResponse = (data: Partial): BillingV2Type => {
export const billingLogic = kea([
path(['scenes', 'billing', 'billingLogic']),
actions({
+ setProductSpecificAlert: (productSpecificAlert: BillingAlertConfig | null) => ({ productSpecificAlert }),
+ setScrollToProductKey: (scrollToProductKey: ProductKey | null) => ({ scrollToProductKey }),
setShowLicenseDirectInput: (show: boolean) => ({ show }),
reportBillingAlertShown: (alertConfig: BillingAlertConfig) => ({ alertConfig }),
reportBillingAlertActionClicked: (alertConfig: BillingAlertConfig) => ({ alertConfig }),
@@ -68,6 +73,18 @@ export const billingLogic = kea([
actions: [userLogic, ['loadUser'], eventUsageLogic, ['reportProductUnsubscribed']],
}),
reducers({
+ scrollToProductKey: [
+ null as ProductKey | null,
+ {
+ setScrollToProductKey: (_, { scrollToProductKey }) => scrollToProductKey,
+ },
+ ],
+ productSpecificAlert: [
+ null as BillingAlertConfig | null,
+ {
+ setProductSpecificAlert: (_, { productSpecificAlert }) => productSpecificAlert,
+ },
+ ],
showLicenseDirectInput: [
false,
{
@@ -146,8 +163,12 @@ export const billingLogic = kea([
},
],
billingAlert: [
- (s) => [s.billing, s.preflight, s.projectedTotalAmountUsd],
- (billing, preflight, projectedTotalAmountUsd): BillingAlertConfig | undefined => {
+ (s) => [s.billing, s.preflight, s.projectedTotalAmountUsd, s.productSpecificAlert],
+ (billing, preflight, projectedTotalAmountUsd, productSpecificAlert): BillingAlertConfig | undefined => {
+ if (productSpecificAlert) {
+ return productSpecificAlert
+ }
+
if (!billing || !preflight?.cloud) {
return
}
@@ -322,6 +343,10 @@ export const billingLogic = kea([
actions.setActivateLicenseValues({ license: hash.license })
actions.submitActivateLicense()
}
+ if (_search.products) {
+ const products = _search.products.split(',')
+ actions.setScrollToProductKey(products[0])
+ }
actions.setRedirectPath()
actions.setIsOnboarding()
},
diff --git a/frontend/src/scenes/billing/billingProductLogic.ts b/frontend/src/scenes/billing/billingProductLogic.ts
index fb5725332c568..5d78ef5ac7e81 100644
--- a/frontend/src/scenes/billing/billingProductLogic.ts
+++ b/frontend/src/scenes/billing/billingProductLogic.ts
@@ -1,5 +1,6 @@
-import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea'
+import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import posthog from 'posthog-js'
+import React from 'react'
import { BillingProductV2AddonType, BillingProductV2Type, BillingV2PlanType, BillingV2TierType } from '~/types'
@@ -9,15 +10,27 @@ import type { billingProductLogicType } from './billingProductLogicType'
const DEFAULT_BILLING_LIMIT = 500
+export interface BillingProductLogicProps {
+ product: BillingProductV2Type | BillingProductV2AddonType
+ billingLimitInputRef?: React.MutableRefObject
+}
+
export const billingProductLogic = kea([
+ props({} as BillingProductLogicProps),
key((props) => props.product.type),
path(['scenes', 'billing', 'billingProductLogic']),
connect({
- values: [billingLogic, ['billing', 'isUnlicensedDebug']],
- actions: [billingLogic, ['loadBillingSuccess', 'updateBillingLimitsSuccess', 'deactivateProduct']],
- }),
- props({
- product: {} as BillingProductV2Type | BillingProductV2AddonType,
+ values: [billingLogic, ['billing', 'isUnlicensedDebug', 'scrollToProductKey']],
+ actions: [
+ billingLogic,
+ [
+ 'loadBillingSuccess',
+ 'updateBillingLimitsSuccess',
+ 'deactivateProduct',
+ 'setProductSpecificAlert',
+ 'setScrollToProductKey',
+ ],
+ ],
}),
actions({
setIsEditingBillingLimit: (isEditingBillingLimit: boolean) => ({ isEditingBillingLimit }),
@@ -217,5 +230,40 @@ export const billingProductLogic = kea([
})
actions.setSurveyID('')
},
+ setScrollToProductKey: ({ scrollToProductKey }) => {
+ if (scrollToProductKey && scrollToProductKey === props.product.type) {
+ const { currentPlan } = values.currentAndUpgradePlans
+
+ if (currentPlan.initial_billing_limit) {
+ actions.setProductSpecificAlert({
+ status: 'warning',
+ title: 'Billing Limit Automatically Applied',
+ pathName: '/organization/billing',
+ dismissKey: `auto-apply-billing-limit-${props.product.type}`,
+ message: `To protect your costs and ours, we've automatically applied a $${currentPlan?.initial_billing_limit} billing limit for ${props.product.name}.`,
+ action: {
+ onClick: () => {
+ actions.setIsEditingBillingLimit(true)
+ setTimeout(() => {
+ if (props.billingLimitInputRef?.current) {
+ props.billingLimitInputRef?.current.focus()
+ props.billingLimitInputRef?.current.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ })
+ }
+ }, 0)
+ },
+ children: 'Update billing limit',
+ },
+ })
+ }
+ }
+ },
+ })),
+ events(({ actions, values }) => ({
+ afterMount: () => {
+ actions.setScrollToProductKey(values.scrollToProductKey)
+ },
})),
])
diff --git a/frontend/src/scenes/cohorts/CohortEdit.tsx b/frontend/src/scenes/cohorts/CohortEdit.tsx
index 36025ad8fd4c1..edbf5ddd46559 100644
--- a/frontend/src/scenes/cohorts/CohortEdit.tsx
+++ b/frontend/src/scenes/cohorts/CohortEdit.tsx
@@ -1,5 +1,4 @@
import { LemonDivider } from '@posthog/lemon-ui'
-import { Divider } from 'antd'
import { UploadFile } from 'antd/es/upload/interface'
import Dragger from 'antd/lib/upload/Dragger'
import { useActions, useValues } from 'kea'
@@ -9,6 +8,7 @@ import { NotFound } from 'lib/components/NotFound'
import { PageHeader } from 'lib/components/PageHeader'
import { CohortTypeEnum } from 'lib/constants'
import { Field } from 'lib/forms/Field'
+import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { IconUploadFile } from 'lib/lemon-ui/icons'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { More } from 'lib/lemon-ui/LemonButton/More'
@@ -30,6 +30,7 @@ import { Query } from '~/queries/Query/Query'
import { AvailableFeature, NotebookNodeType } from '~/types'
export function CohortEdit({ id }: CohortLogicProps): JSX.Element {
+ const is3000 = useFeatureFlag('POSTHOG_3000')
const logicProps = { id }
const logic = cohortEditLogic(logicProps)
const { deleteCohort, setOuterGroupsType, setQuery, duplicateCohort } = useActions(logic)
@@ -127,8 +128,8 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element {
}
/>
-
-
+ {!is3000 &&
}
+
@@ -212,7 +213,7 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element {
) : (
<>
-
+
Matching criteria
@@ -237,7 +238,7 @@ export function CohortEdit({ id }: CohortLogicProps): JSX.Element {
{/* The typeof here is needed to pass the cohort id to the query below. Using `isNewCohort` won't work */}
{typeof cohort.id === 'number' && (
<>
-
+
Persons in this cohort
diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx
index bb8a881b866db..9b5968364a232 100644
--- a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx
+++ b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx
@@ -1,6 +1,6 @@
import './CohortCriteriaRowBuilder.scss'
-import { Col, Divider } from 'antd'
+import { Divider } from 'antd'
import clsx from 'clsx'
import { useActions } from 'kea'
import { Field as KeaField } from 'kea-forms'
@@ -40,7 +40,7 @@ export function CohortCriteriaRowBuilder({
const renderFieldComponent = (_field: Field, i: number): JSX.Element => {
return (
-
+
{renderField[_field.type]({
fieldKey: _field.fieldKey,
criteria,
@@ -48,7 +48,7 @@ export function CohortCriteriaRowBuilder({
...(_field.groupTypeFieldKey ? { groupTypeFieldKey: _field.groupTypeFieldKey } : {}),
onChange: (newCriteria) => setCriteria(newCriteria, groupIndex, index),
} as CohortFieldProps)}
-
+
)
}
@@ -97,7 +97,7 @@ export function CohortCriteriaRowBuilder({
}}
>
<>
-
+
{renderField[FilterType.Behavioral]({
fieldKey: 'value',
criteria,
@@ -106,7 +106,7 @@ export function CohortCriteriaRowBuilder({
onChangeType?.(newCriteria['value'] ?? BehavioralEventType.PerformEvent)
},
})}
-
+
>
diff --git a/frontend/src/scenes/cohorts/cohortUtils.tsx b/frontend/src/scenes/cohorts/cohortUtils.tsx
index b856d8d2d1a5c..d8d701daeb13a 100644
--- a/frontend/src/scenes/cohorts/cohortUtils.tsx
+++ b/frontend/src/scenes/cohorts/cohortUtils.tsx
@@ -90,7 +90,7 @@ export function isValidCohortGroup(criteria: AnyCohortGroupType): boolean {
export function createCohortFormData(cohort: CohortType): FormData {
const rawCohort = {
...(cohort.name ? { name: cohort.name } : {}),
- ...(cohort.description ? { description: cohort.description } : {}),
+ ...{ description: cohort.description ?? '' },
...(cohort.csv ? { csv: cohort.csv } : {}),
...(cohort.is_static ? { is_static: cohort.is_static } : {}),
filters: JSON.stringify(
diff --git a/frontend/src/scenes/dashboard/Dashboard.scss b/frontend/src/scenes/dashboard/Dashboard.scss
index f35d1decb41af..fe4c80e270a4a 100644
--- a/frontend/src/scenes/dashboard/Dashboard.scss
+++ b/frontend/src/scenes/dashboard/Dashboard.scss
@@ -37,3 +37,11 @@
}
}
}
+
+.DashboardTemplates__option {
+ border: 1px solid var(--border);
+
+ &:hover {
+ border-color: var(--primary-3000-hover);
+ }
+}
diff --git a/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx b/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx
index 4286da5031b80..00889a9e1e5aa 100644
--- a/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx
+++ b/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx
@@ -1,8 +1,9 @@
+import { IconPin, IconPinFilled, IconShare } from '@posthog/icons'
import { LemonInput, LemonSelect } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
import { DashboardPrivilegeLevel } from 'lib/constants'
-import { IconCottage, IconLock, IconPinFilled, IconPinOutline, IconShare } from 'lib/lemon-ui/icons'
+import { IconCottage, IconLock } from 'lib/lemon-ui/icons'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { More } from 'lib/lemon-ui/LemonButton/More'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
@@ -72,7 +73,7 @@ export function DashboardsTable({
: () => pinDashboard(id, DashboardEventSource.DashboardsList)
}
tooltip={pinned ? 'Unpin dashboard' : 'Pin dashboard'}
- icon={pinned ? : }
+ icon={pinned ? : }
/>
)
},
@@ -217,28 +218,31 @@ export function DashboardsTable({
/>
- setFilters({ pinned: !filters.pinned })}
- icon={}
- >
- Pinned
-
-
-
-
setFilters({ shared: !filters.shared })}
- icon={}
- >
- Shared
-
+
Filter to:
+
+ setFilters({ pinned: !filters.pinned })}
+ icon={}
+ >
+ Pinned
+
+
+
+ setFilters({ shared: !filters.shared })}
+ icon={}
+ >
+ Shared
+
+
Created by:
diff --git a/frontend/src/scenes/dashboard/dashboards/NoDashboards.tsx b/frontend/src/scenes/dashboard/dashboards/NoDashboards.tsx
index fe5beead0cce0..f8797c2e0b6da 100644
--- a/frontend/src/scenes/dashboard/dashboards/NoDashboards.tsx
+++ b/frontend/src/scenes/dashboard/dashboards/NoDashboards.tsx
@@ -1,47 +1,51 @@
-// eslint-disable-next-line no-restricted-imports
-import { AppstoreAddOutlined } from '@ant-design/icons'
-import { Card } from 'antd'
import { useActions } from 'kea'
import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic'
export const NoDashboards = (): JSX.Element => {
- const { addDashboard } = useActions(newDashboardLogic)
-
return (
Create your first dashboard:
-
- addDashboard({
- name: 'New Dashboard',
- useTemplate: '',
- })
- }
- >
-
-
-
+
)
}
+
+const Option = ({
+ title,
+ description,
+ template,
+}: {
+ title: string
+ description: string
+ template: { name: string; template: string }
+}): JSX.Element => {
+ const { addDashboard } = useActions(newDashboardLogic)
+
+ const onClick = (): void => {
+ addDashboard({
+ name: template.name,
+ useTemplate: template.template,
+ })
+ }
+
+ return (
+
+
{title}
+
{description}
+
+ )
+}
diff --git a/frontend/src/scenes/data-management/definition/DefinitionView.tsx b/frontend/src/scenes/data-management/definition/DefinitionView.tsx
index 107c63d249fad..400da13b5b2d6 100644
--- a/frontend/src/scenes/data-management/definition/DefinitionView.tsx
+++ b/frontend/src/scenes/data-management/definition/DefinitionView.tsx
@@ -1,7 +1,7 @@
import './Definition.scss'
import { TZLabel } from '@posthog/apps-common'
-import { Divider } from 'antd'
+import { LemonDivider } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { combineUrl } from 'kea-router/lib/utils'
@@ -194,7 +194,7 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element {
>
}
/>
-
+
{isEvent && (
<>
@@ -216,11 +216,11 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element {
/>
)}
-
+
{isEvent && definition.id !== 'new' && (
<>
-
+
Matching events
diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.scss b/frontend/src/scenes/feature-flags/FeatureFlag.scss
index 0b72506c172cb..319512c5f7670 100644
--- a/frontend/src/scenes/feature-flags/FeatureFlag.scss
+++ b/frontend/src/scenes/feature-flags/FeatureFlag.scss
@@ -64,6 +64,10 @@
}
.FeatureConditionCard {
+ .posthog-3000 & {
+ background: var(--bg-light);
+ }
+
.FeatureConditionCard--border--highlight {
border-color: var(--primary-3000);
}
diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx
index ded4d0f3278ee..8c2ef3d75606e 100644
--- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx
+++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx
@@ -16,6 +16,7 @@ import { Field } from 'lib/forms/Field'
import { IconDelete, IconLock, IconPlus, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
+import { More } from 'lib/lemon-ui/LemonButton/More'
import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput'
@@ -60,7 +61,6 @@ import {
Resource,
} from '~/types'
-import { organizationLogic } from '../organizationLogic'
import { AnalysisTab } from './FeatureFlagAnalysisTab'
import { FeatureFlagAutoRollback } from './FeatureFlagAutoRollout'
import { FeatureFlagCodeExample } from './FeatureFlagCodeExample'
@@ -87,10 +87,17 @@ function focusVariantKeyField(index: number): void {
}
export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {
- const { props, featureFlag, featureFlagLoading, featureFlagMissing, isEditingFlag, recordingFilterForFlag } =
- useValues(featureFlagLogic)
+ const {
+ props,
+ featureFlag,
+ featureFlagLoading,
+ featureFlagMissing,
+ isEditingFlag,
+ recordingFilterForFlag,
+ newCohortLoading,
+ } = useValues(featureFlagLogic)
const { featureFlags } = useValues(enabledFeaturesLogic)
- const { deleteFeatureFlag, editFeatureFlag, loadFeatureFlag, triggerFeatureFlagUpdate } =
+ const { deleteFeatureFlag, editFeatureFlag, loadFeatureFlag, triggerFeatureFlagUpdate, createStaticCohort } =
useActions(featureFlagLogic)
const { addableRoles, unfilteredAddableRolesLoading, rolesToAdd, derivedRoles } = useValues(
@@ -100,8 +107,6 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {
featureFlagPermissionsLogic({ flagId: featureFlag.id })
)
- const { currentOrganization } = useValues(organizationLogic)
-
const { tags } = useValues(tagsModel)
const { hasAvailableFeature } = useValues(userLogic)
@@ -158,8 +163,7 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {
})
}
- const hasMultipleProjects = (currentOrganization?.teams?.length ?? 0) > 1
- if (featureFlags[FEATURE_FLAGS.MULTI_PROJECT_FEATURE_FLAGS] && hasMultipleProjects) {
+ if (featureFlags[FEATURE_FLAGS.MULTI_PROJECT_FEATURE_FLAGS]) {
tabs.push({
label: 'Projects',
key: FeatureFlagsTab.PROJECTS,
@@ -530,6 +534,58 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {
buttons={
<>
+
+
+ View Recordings
+
+ {featureFlags[
+ FEATURE_FLAGS.FEATURE_FLAG_COHORT_CREATION
+ ] && (
+ {
+ createStaticCohort()
+ }}
+ fullWidth
+ >
+ Create Cohort
+
+ )}
+
+ {
+ deleteFeatureFlag(featureFlag)
+ }}
+ disabledReason={
+ featureFlagLoading
+ ? 'Loading...'
+ : !featureFlag.can_edit
+ ? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator."
+ : (featureFlag.features?.length || 0) > 0
+ ? 'This feature flag is in use with an early access feature. Delete the early access feature to delete this flag'
+ : (featureFlag.experiment_set?.length || 0) > 0
+ ? 'This feature flag is linked to an experiment. Delete the experiment to delete this flag'
+ : null
+ }
+ >
+ Delete feature flag
+
+ >
+ }
+ />
+
-
- View Recordings
-
-
- {
- deleteFeatureFlag(featureFlag)
- }}
- disabledReason={
- featureFlagLoading
- ? 'Loading...'
- : !featureFlag.can_edit
- ? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator."
- : (featureFlag.features?.length || 0) > 0
- ? 'This feature flag is in use with an early access feature. Delete the early access feature to delete this flag'
- : (featureFlag.experiment_set?.length || 0) > 0
- ? 'This feature flag is linked to an experiment. Delete the experiment to delete this flag'
- : null
- }
- >
- Delete feature flag
-
=> {
]
}
-export default function FeatureFlagProjects(): JSX.Element {
+function InfoBanner(): JSX.Element {
+ const { currentOrganization } = useValues(organizationLogic)
+ const { featureFlag } = useValues(featureFlagLogic)
+ const hasMultipleProjects = (currentOrganization?.teams?.length ?? 0) > 1
+
+ const isMember =
+ !currentOrganization?.membership_level ||
+ currentOrganization.membership_level < OrganizationMembershipLevel.Admin
+
+ let text
+
+ if (isMember && !hasMultipleProjects) {
+ text = `You currently have access to only one project. If your organization manages multiple projects and you wish to copy this feature flag across them, request project access from your administrator.`
+ } else if (!hasMultipleProjects) {
+ text = `This feature enables the copying of a feature flag across different projects. Once additional projects are added within your organization, you'll be able to replicate this flag to them.`
+ } else if (!featureFlag.can_edit) {
+ text = `You don't have the necessary permissions to copy this flag to another project. Contact your administrator to request editing rights.`
+ } else {
+ return <>>
+ }
+
+ return (
+
+ {text}
+
+ )
+}
+
+function FeatureFlagCopySection(): JSX.Element {
const { featureFlag, copyDestinationProject, projectsWithCurrentFlag, featureFlagCopyLoading } =
useValues(featureFlagLogic)
- const { setCopyDestinationProject, loadProjectsWithCurrentFlag, copyFlag } = useActions(featureFlagLogic)
+ const { setCopyDestinationProject, copyFlag } = useActions(featureFlagLogic)
const { currentOrganization } = useValues(organizationLogic)
const { currentTeam } = useValues(teamLogic)
+ const hasMultipleProjects = (currentOrganization?.teams?.length ?? 0) > 1
+
+ return hasMultipleProjects && featureFlag.can_edit ? (
+ <>
+ Feature flag copy
+ Copy your flag and its configuration to another project.
+
+
+
Key
+
+ {featureFlag.key}
+
+
+
+
+
Destination project
+
setCopyDestinationProject(id)}
+ options={
+ currentOrganization?.teams
+ ?.map((team) => ({ value: team.id, label: team.name }))
+ .filter((option) => option.value !== currentTeam?.id) || []
+ }
+ className="min-w-40"
+ />
+
+
+
+
}
+ onClick={() => copyFlag()}
+ className="w-28 max-w-28"
+ >
+ {projectsWithCurrentFlag.find((p) => Number(p.team_id) === copyDestinationProject)
+ ? 'Update'
+ : 'Copy'}
+
+
+
+ >
+ ) : (
+ <>>
+ )
+}
+
+export default function FeatureFlagProjects(): JSX.Element {
+ const { projectsWithCurrentFlag } = useValues(featureFlagLogic)
+ const { loadProjectsWithCurrentFlag } = useActions(featureFlagLogic)
+
useEffect(() => {
loadProjectsWithCurrentFlag()
}, [])
return (
- {featureFlag.can_edit ? (
- <>
-
Feature flag copy
-
Copy your flag and its configuration to another project.
-
-
-
Key
-
- {featureFlag.key}
-
-
-
-
-
Destination project
-
setCopyDestinationProject(id)}
- options={
- currentOrganization?.teams
- ?.map((team) => ({ value: team.id, label: team.name }))
- .filter((option) => option.value !== currentTeam?.id) || []
- }
- className="min-w-40"
- />
-
-
-
-
}
- onClick={() => copyFlag()}
- className="w-28 max-w-28"
- >
- {projectsWithCurrentFlag.find((p) => Number(p.team_id) === copyDestinationProject)
- ? 'Update'
- : 'Copy'}
-
-
-
- >
- ) : (
-
- You currently cannot copy this flag to another project. Contact your administrator to request
- editing rights.
-
- )}
+
+
([
},
},
],
+ newCohort: [
+ null as CohortType | null,
+ {
+ createStaticCohort: async () => {
+ if (props.id && props.id !== 'new' && props.id !== 'link') {
+ return (await api.featureFlags.createStaticCohort(props.id)).cohort
+ }
+ return null
+ },
+ },
+ ],
projectsWithCurrentFlag: {
__default: [] as OrganizationFeatureFlag[],
loadProjectsWithCurrentFlag: async () => {
@@ -792,6 +804,16 @@ export const featureFlagLogic = kea([
actions.loadProjectsWithCurrentFlag()
actions.setCopyDestinationProject(null)
},
+ createStaticCohortSuccess: ({ newCohort }) => {
+ if (newCohort) {
+ lemonToast.success('Static cohort created successfully', {
+ button: {
+ label: 'View cohort',
+ action: () => router.actions.push(urls.cohort(newCohort.id)),
+ },
+ })
+ }
+ },
})),
selectors({
sentryErrorCount: [(s) => [s.sentryStats], (stats) => stats.total_count],
diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx
index 06830c7680800..e99592c5176ac 100644
--- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx
+++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx
@@ -4,7 +4,7 @@ import './EmptyStates.scss'
import { PlusCircleOutlined, ThunderboltFilled } from '@ant-design/icons'
import { IconWarning } from '@posthog/icons'
import { LemonButton } from '@posthog/lemon-ui'
-import { Button, Empty } from 'antd'
+import { Empty } from 'antd'
import { useActions, useValues } from 'kea'
import { BuilderHog3 } from 'lib/components/hedgehogs'
import { supportLogic } from 'lib/components/Support/supportLogic'
@@ -315,17 +315,19 @@ export function SavedInsightsEmptyState(): JSX.Element {
{description}
)}
{tab !== SavedInsightsTabs.Favorites && (
-
- }
- className="add-insight-button"
- >
- New Insight
-
-
+
+
+ }
+ className="add-insight-button"
+ >
+ New Insight
+
+
+
)}
diff --git a/frontend/src/scenes/instance/SystemStatus/KafkaInspectorTab.tsx b/frontend/src/scenes/instance/SystemStatus/KafkaInspectorTab.tsx
index c4d9eb899bce8..cbf13c19dcf59 100644
--- a/frontend/src/scenes/instance/SystemStatus/KafkaInspectorTab.tsx
+++ b/frontend/src/scenes/instance/SystemStatus/KafkaInspectorTab.tsx
@@ -1,4 +1,4 @@
-import { Button, Col, Divider, Row } from 'antd'
+import { LemonButton, LemonDivider } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { Field, Form } from 'kea-forms'
import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
@@ -13,7 +13,7 @@ export function KafkaInspectorTab(): JSX.Element {
Kafka Inspector
Debug Kafka messages using the inspector tool.
-
+
diff --git a/frontend/src/scenes/instance/SystemStatus/index.tsx b/frontend/src/scenes/instance/SystemStatus/index.tsx
index 9840cd5dd6964..69ccf9b44bc11 100644
--- a/frontend/src/scenes/instance/SystemStatus/index.tsx
+++ b/frontend/src/scenes/instance/SystemStatus/index.tsx
@@ -1,7 +1,6 @@
import './index.scss'
-import { Link } from '@posthog/lemon-ui'
-import { Alert } from 'antd'
+import { LemonBanner, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { PageHeader } from 'lib/components/PageHeader'
import { FEATURE_FLAGS } from 'lib/constants'
@@ -106,44 +105,35 @@ export function SystemStatus(): JSX.Element {
>
}
/>
- {error && (
-
An unknown error occurred. Please try again or contact us.}
- type="error"
- showIcon
- />
- )}
- {siteUrlMisconfigured && (
-
- Your SITE_URL
environment variable seems misconfigured. Your{' '}
- SITE_URL
is set to{' '}
-
- {preflight?.site_url}
- {' '}
- but you're currently browsing this page from{' '}
-
- {window.location.origin}
-
- . In order for PostHog to work properly, please set this to the origin where your instance
- is hosted.{' '}
-
- Learn more
-
- >
- }
- showIcon
- type="warning"
- style={{ marginBottom: 32 }}
- />
- )}
+
+ {error && (
+
+ Something went wrong
+ {error || 'An unknown error occurred. Please try again or contact us.'}
+
+ )}
+ {siteUrlMisconfigured && (
+
+ Your SITE_URL
environment variable seems misconfigured. Your SITE_URL
{' '}
+ is set to{' '}
+
+ {preflight?.site_url}
+ {' '}
+ but you're currently browsing this page from{' '}
+
+ {window.location.origin}
+
+ . In order for PostHog to work properly, please set this to the origin where your instance is
+ hosted.
+
+ )}
+
diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss
index 179f475205dbf..1d4fa2d2021a3 100644
--- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss
+++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss
@@ -56,7 +56,7 @@
}
&--selected {
- --border-color: var(--primary-3000);
+ --border-color: var(--border-bold);
}
&--auto-hide-metadata {
diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx
index cba90e520677c..d2c96c06ee054 100644
--- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx
+++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx
@@ -75,7 +75,17 @@ function NodeWrapper(props: NodeWrapperP
// nodeId can start null, but should then immediately be generated
const nodeLogic = useMountedLogic(notebookNodeLogic(logicProps))
const { resizeable, expanded, actions, nodeId } = useValues(nodeLogic)
- const { setExpanded, deleteNode, toggleEditing, insertOrSelectNextLine } = useActions(nodeLogic)
+ const { setRef, setExpanded, deleteNode, toggleEditing, insertOrSelectNextLine } = useActions(nodeLogic)
+
+ const { ref: inViewRef, inView } = useInView({ triggerOnce: true })
+
+ const setRefs = useCallback(
+ (node) => {
+ setRef(node)
+ inViewRef(node)
+ },
+ [inViewRef]
+ )
useEffect(() => {
// TRICKY: child nodes mount the parent logic so we need to control the mounting / unmounting directly in this component
@@ -92,7 +102,6 @@ function NodeWrapper(props: NodeWrapperP
mountedNotebookLogic,
})
- const [ref, inView] = useInView({ triggerOnce: true })
const contentRef = useRef(null)
// If resizeable is true then the node attr "height" is required
@@ -136,7 +145,7 @@ function NodeWrapper(props: NodeWrapperP
([
initializeNode: true,
setMessageListeners: (listeners: NotebookNodeMessagesListeners) => ({ listeners }),
setTitlePlaceholder: (titlePlaceholder: string) => ({ titlePlaceholder }),
+ setRef: (ref: HTMLElement | null) => ({ ref }),
}),
connect((props: NotebookNodeLogicProps) => ({
@@ -71,6 +72,13 @@ export const notebookNodeLogic = kea
([
})),
reducers(({ props }) => ({
+ ref: [
+ null as HTMLElement | null,
+ {
+ setRef: (_, { ref }) => ref,
+ unregisterNodeLogic: () => null,
+ },
+ ],
expanded: [
props.startExpanded ?? true,
{
@@ -246,7 +254,9 @@ export const notebookNodeLogic = kea([
props.updateAttributes(attributes)
},
toggleEditing: ({ visible }) => {
- const shouldShowThis = typeof visible === 'boolean' ? visible : !values.notebookLogic.values.editingNodeId
+ const shouldShowThis =
+ typeof visible === 'boolean' ? visible : values.notebookLogic.values.editingNodeId !== values.nodeId
+
props.notebookLogic.actions.setEditingNodeId(shouldShowThis ? values.nodeId : null)
},
initializeNode: () => {
diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.scss b/frontend/src/scenes/notebooks/Notebook/Notebook.scss
index ed584c90842d8..9b0139e499d63 100644
--- a/frontend/src/scenes/notebooks/Notebook/Notebook.scss
+++ b/frontend/src/scenes/notebooks/Notebook/Notebook.scss
@@ -147,14 +147,6 @@
}
}
- &--editable {
- .NotebookEditor .ProseMirror {
- // Add some padding to help clicking below the last element
- padding-bottom: 10rem;
- flex: 1;
- }
- }
-
.NotebookColumn {
position: relative;
width: 0;
@@ -191,6 +183,11 @@
.NotebookColumn__content {
width: var(--notebook-column-left-width);
transform: translateX(-100%);
+
+ > .LemonWidget .LemonWidget__content {
+ max-height: var(--notebook-sidebar-height);
+ overflow: auto;
+ }
}
}
@@ -218,12 +215,27 @@
}
}
+ &--editable {
+ .NotebookEditor .ProseMirror {
+ // Add some padding to help clicking below the last element
+ padding-bottom: 10rem;
+ flex: 1;
+ }
+
+ .NotebookColumn--left.NotebookColumn--showing {
+ & + .NotebookEditor {
+ .ProseMirror {
+ // Add a lot of padding to allow the entire column to always be on screen
+ padding-bottom: 100vh;
+ }
+ }
+ }
+ }
+
.NotebookHistory {
flex: 1;
display: flex;
flex-direction: column;
- height: var(--notebook-sidebar-height);
- overflow: hidden;
}
.NotebookInlineMenu {
@@ -236,13 +248,6 @@
}
}
- .NotebookColumnLeft__widget {
- > .LemonWidget__content {
- max-height: calc(100vh - 220px);
- overflow: auto;
- }
- }
-
.LemonTable__content > table > thead {
position: sticky;
top: 0;
diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookColumnLeft.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookColumnLeft.tsx
index 1f91e5ebb1960..871f352760c89 100644
--- a/frontend/src/scenes/notebooks/Notebook/NotebookColumnLeft.tsx
+++ b/frontend/src/scenes/notebooks/Notebook/NotebookColumnLeft.tsx
@@ -1,8 +1,8 @@
import { LemonButton } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { BuiltLogic, useActions, useValues } from 'kea'
-import { IconEyeVisible } from 'lib/lemon-ui/icons'
import { LemonWidget } from 'lib/lemon-ui/LemonWidget'
+import { useEffect, useRef, useState } from 'react'
import { notebookNodeLogicType } from '../Nodes/notebookNodeLogicType'
import { NotebookHistory } from './NotebookHistory'
@@ -17,7 +17,7 @@ export const NotebookColumnLeft = (): JSX.Element | null => {
'NotebookColumn--showing': isShowingLeftColumn,
})}
>
-
+ {editingNodeLogic ? : null}
{isShowingLeftColumn ? (
editingNodeLogic ? (
@@ -31,6 +31,41 @@ export const NotebookColumnLeft = (): JSX.Element | null => {
)
}
+export const NotebookNodeSettingsOffset = ({ logic }: { logic: BuiltLogic
}): JSX.Element => {
+ const { ref } = useValues(logic)
+ const offsetRef = useRef(null)
+ const [height, setHeight] = useState(0)
+
+ useEffect(() => {
+ // Interval to check the relative positions of the node and the offset div
+ // updating the height so that it always is inline
+ const updateHeight = (): void => {
+ if (ref && offsetRef.current) {
+ const newHeight = ref.getBoundingClientRect().top - offsetRef.current.getBoundingClientRect().top
+
+ if (height !== newHeight) {
+ setHeight(newHeight)
+ }
+ }
+ }
+
+ const interval = setInterval(updateHeight, 100)
+ updateHeight()
+
+ return () => clearInterval(interval)
+ }, [ref, offsetRef.current, height])
+
+ return (
+
+ )
+}
+
export const NotebookNodeSettingsWidget = ({ logic }: { logic: BuiltLogic }): JSX.Element => {
const { setEditingNodeId } = useActions(notebookLogic)
const { Settings, nodeAttributes, title } = useValues(logic)
@@ -42,16 +77,21 @@ export const NotebookNodeSettingsWidget = ({ logic }: { logic: BuiltLogic
- } size="small" status="primary" onClick={() => selectNode()} />
setEditingNodeId(null)}>
Done
>
}
>
- {Settings ? (
-
- ) : null}
+ selectNode()}>
+ {Settings ? (
+
+ ) : null}
+
)
}
diff --git a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx
index 874a7180a8bc4..0a7bf012d8e19 100644
--- a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx
+++ b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx
@@ -3,6 +3,7 @@ import {
IconFunnels,
IconHogQL,
IconLifecycle,
+ IconPeople,
IconRetention,
IconRewindPlay,
IconStickiness,
@@ -17,7 +18,7 @@ import { ReactRenderer } from '@tiptap/react'
import Suggestion from '@tiptap/suggestion'
import Fuse from 'fuse.js'
import { useValues } from 'kea'
-import { IconBold, IconCohort, IconItalic } from 'lib/lemon-ui/icons'
+import { IconBold, IconItalic } from 'lib/lemon-ui/icons'
import { Popover } from 'lib/lemon-ui/Popover'
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'
@@ -98,7 +99,7 @@ const TEXT_CONTROLS: SlashCommandsItem[] = [
const SLASH_COMMANDS: SlashCommandsItem[] = [
{
title: 'Trend',
- search: 'trend insight',
+ search: 'graph trend insight',
icon: ,
command: (chain, pos) =>
chain.insertContentAt(
@@ -177,7 +178,7 @@ const SLASH_COMMANDS: SlashCommandsItem[] = [
},
{
title: 'Paths',
- search: 'paths insight',
+ search: 'user paths insight',
icon: ,
command: (chain, pos) =>
chain.insertContentAt(
@@ -283,9 +284,9 @@ order by count() desc
),
},
{
- title: 'Persons',
- search: 'people users',
- icon: ,
+ title: 'People',
+ search: 'persons users',
+ icon: ,
command: (chain, pos) =>
chain.insertContentAt(
pos,
@@ -300,14 +301,14 @@ order by count() desc
),
},
{
- title: 'Session Replays',
- search: 'recordings video',
+ title: 'Session recordings',
+ search: 'video replay',
icon: ,
command: (chain, pos) => chain.insertContentAt(pos, { type: NotebookNodeType.RecordingPlaylist, attrs: {} }),
},
{
title: 'Image',
- search: 'picture',
+ search: 'picture gif',
icon: ,
command: async (chain, pos) => {
// Trigger upload followed by insert
diff --git a/frontend/src/scenes/notebooks/NotebookScene.tsx b/frontend/src/scenes/notebooks/NotebookScene.tsx
index d0a81dff63eed..0d0b2baa69f5e 100644
--- a/frontend/src/scenes/notebooks/NotebookScene.tsx
+++ b/frontend/src/scenes/notebooks/NotebookScene.tsx
@@ -1,11 +1,11 @@
import './NotebookScene.scss'
+import { IconInfo, IconOpenSidebar } from '@posthog/icons'
import { LemonButton, LemonTag } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { NotFound } from 'lib/components/NotFound'
import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator'
import { FEATURE_FLAGS } from 'lib/constants'
-import { IconArrowRight, IconHelpOutline } from 'lib/lemon-ui/icons'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { SceneExport } from 'scenes/sceneTypes'
@@ -51,7 +51,7 @@ export function NotebookScene(): JSX.Element {
return (
- This Notebook is open in the side panel
+ This Notebook is open in the side panel
@@ -87,7 +87,7 @@ export function NotebookScene(): JSX.Element {
}
+ icon={}
size={buttonSize}
onClick={() => {
if (selectedNotebook === LOCAL_NOTEBOOK_TEMPLATES[0].short_id && visibility === 'visible') {
@@ -112,11 +112,11 @@ export function NotebookScene(): JSX.Element {
tooltip={
<>
Opens the notebook in a side panel, that can be accessed from anywhere in the PostHog
- app. This is great for dragging and dropping elements like Insights, Recordings or even
- Feature Flags into your active Notebook.
+ app. This is great for dragging and dropping elements like insights, recordings or even
+ feature flags into your active notebook.
>
}
- sideIcon={}
+ sideIcon={}
>
Open in side panel
diff --git a/frontend/src/scenes/plugins/edit/PluginDrawer.tsx b/frontend/src/scenes/plugins/edit/PluginDrawer.tsx
index 443d76095aaf2..0fbf516201734 100644
--- a/frontend/src/scenes/plugins/edit/PluginDrawer.tsx
+++ b/frontend/src/scenes/plugins/edit/PluginDrawer.tsx
@@ -1,7 +1,7 @@
import { IconCode } from '@posthog/icons'
-import { LemonButton, LemonTag, Link } from '@posthog/lemon-ui'
+import { LemonButton, LemonSwitch, LemonTag, Link } from '@posthog/lemon-ui'
import { PluginConfigChoice, PluginConfigSchema } from '@posthog/plugin-scaffold'
-import { Form, Switch } from 'antd'
+import { Form } from 'antd'
import { useActions, useValues } from 'kea'
import { Drawer } from 'lib/components/Drawer'
import { MOCK_NODE_PROCESS } from 'lib/constants'
@@ -32,10 +32,10 @@ function EnabledDisabledSwitch({
onChange?: (value: boolean) => void
}): JSX.Element {
return (
- <>
-
- {value ? 'Enabled' : 'Disabled'}
- >
+
+
+ {value ? 'Enabled' : 'Disabled'}
+
)
}
diff --git a/frontend/src/scenes/project-homepage/ProjectHomePageCompactListItem.tsx b/frontend/src/scenes/project-homepage/ProjectHomePageCompactListItem.tsx
index c61ac0e2bcc8c..2ea324dfaf8b5 100644
--- a/frontend/src/scenes/project-homepage/ProjectHomePageCompactListItem.tsx
+++ b/frontend/src/scenes/project-homepage/ProjectHomePageCompactListItem.tsx
@@ -23,7 +23,7 @@ export function ProjectHomePageCompactListItem({
{title}
-
{subtitle}
+
{subtitle}
{suffix ?
{suffix} : null}
diff --git a/frontend/src/scenes/saved-insights/SavedInsights.tsx b/frontend/src/scenes/saved-insights/SavedInsights.tsx
index d6e7fdb4d99a6..95728c2f578bf 100644
--- a/frontend/src/scenes/saved-insights/SavedInsights.tsx
+++ b/frontend/src/scenes/saved-insights/SavedInsights.tsx
@@ -6,6 +6,8 @@ import {
IconHogQL,
IconLifecycle,
IconRetention,
+ IconStar,
+ IconStarFilled,
IconStickiness,
IconTrends,
IconUserPaths,
@@ -29,8 +31,6 @@ import {
IconPerson,
IconPlusMini,
IconSelectEvents,
- IconStarFilled,
- IconStarOutline,
IconTableChart,
} from 'lib/lemon-ui/icons'
import { LemonButton, LemonButtonWithSideAction, LemonButtonWithSideActionProps } from 'lib/lemon-ui/LemonButton'
@@ -431,10 +431,10 @@ export function SavedInsights(): JSX.Element {
insight.favorited ? (
) : (
-
+
)
}
- tooltip={`${insight.favorited ? 'Add to' : 'Remove from'} favorite insights`}
+ tooltip={`${insight.favorited ? 'Remove from' : 'Add to'} favorite insights`}
/>
{hasDashboardCollaboration && insight.description && (
diff --git a/frontend/src/scenes/saved-insights/newInsightsMenu.tsx b/frontend/src/scenes/saved-insights/newInsightsMenu.tsx
index e60bef8349378..e7dee45af8d99 100644
--- a/frontend/src/scenes/saved-insights/newInsightsMenu.tsx
+++ b/frontend/src/scenes/saved-insights/newInsightsMenu.tsx
@@ -34,7 +34,7 @@ export function overlayForNewInsightMenu(dataAttr: string): ReactNode[] {
>
{listedInsightTypeMetadata.name}
- {listedInsightTypeMetadata.description}
+ {listedInsightTypeMetadata.description}
)
diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts
index e3a653eb095a8..370847c6f4479 100644
--- a/frontend/src/scenes/scenes.ts
+++ b/frontend/src/scenes/scenes.ts
@@ -119,7 +119,7 @@ export const sceneConfigurations: Partial
> = {
},
[Scene.Experiments]: {
projectBased: true,
- name: 'Experiments',
+ name: 'A/B testing',
},
[Scene.Experiment]: {
projectBased: true,
diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx
index 27d35889ea999..73fac6683aa02 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx
+++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx
@@ -142,7 +142,7 @@ export function ItemPerformanceEvent({
expanded,
setExpanded,
}: ItemPerformanceEvent): JSX.Element {
- const [activeTab, setActiveTab] = useState<'timings' | 'headers' | 'payload' | 'response_body'>('timings')
+ const [activeTab, setActiveTab] = useState<'timings' | 'headers' | 'payload' | 'response_body' | 'raw'>('timings')
const bytes = humanizeBytes(item.encoded_body_size || item.decoded_body_size || 0)
const startTime = item.start_time || item.fetch_start || 0
@@ -178,7 +178,11 @@ export function ItemPerformanceEvent({
return acc
}
- if (['response_headers', 'request_headers', 'request_body', 'response_body', 'response_status'].includes(key)) {
+ if (
+ ['response_headers', 'request_headers', 'request_body', 'response_body', 'response_status', 'raw'].includes(
+ key
+ )
+ ) {
return acc
}
@@ -394,6 +398,17 @@ export function ItemPerformanceEvent({
),
}
: false,
+ // raw is only available if the feature flag is enabled
+ // TODO before proper release we should put raw behind its own flag
+ {
+ key: 'raw',
+ label: 'Json',
+ content: (
+
+ {JSON.stringify(item.raw, null, 2)}
+
+ ),
+ },
]}
/>
@@ -472,6 +487,11 @@ function StatusRow({ item }: { item: PerformanceEvent }): JSX.Element | null {
let statusRow = null
let methodRow = null
+ let fromDiskCache = false
+ if (item.transfer_size === 0 && item.response_body && item.response_status && item.response_status < 400) {
+ fromDiskCache = true
+ }
+
if (item.response_status) {
const statusDescription = `${item.response_status} ${friendlyHttpStatus[item.response_status] || ''}`
@@ -485,7 +505,10 @@ function StatusRow({ item }: { item: PerformanceEvent }): JSX.Element | null {
statusRow = (
Status code
-
{statusDescription}
+
+ {statusDescription}
+ {fromDiskCache && (from cache)}
+
)
}
diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx
index a6ed020056f41..d2511521d3b7e 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx
+++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx
@@ -20,27 +20,30 @@ export function Basic(): JSX.Element {
+
/**
* There are defined sections to performance measurement. We may have data for some or all of them
*
@@ -110,85 +111,114 @@ function colorForSection(section: (typeof perfSections)[number]): string {
*
* see https://nicj.net/resourcetiming-in-practice/
*/
-function calculatePerformanceParts(perfEntry: PerformanceEvent): Record {
+export function calculatePerformanceParts(perfEntry: PerformanceEvent): PerformanceMeasures {
const performanceParts: Record = {}
- if (perfEntry.redirect_start && perfEntry.redirect_end) {
- performanceParts['redirect'] = {
- start: perfEntry.redirect_start,
- end: perfEntry.redirect_end,
- color: colorForSection('redirect'),
+ if (isPresent(perfEntry.redirect_start) && isPresent(perfEntry.redirect_end)) {
+ if (perfEntry.redirect_end - perfEntry.redirect_start > 0) {
+ performanceParts['redirect'] = {
+ start: perfEntry.redirect_start,
+ end: perfEntry.redirect_end,
+ color: colorForSection('redirect'),
+ }
}
}
- if (perfEntry.fetch_start && perfEntry.domain_lookup_start) {
- performanceParts['app cache'] = {
- start: perfEntry.fetch_start,
- end: perfEntry.domain_lookup_start,
- color: colorForSection('app cache'),
+ if (isPresent(perfEntry.fetch_start) && isPresent(perfEntry.domain_lookup_start)) {
+ if (perfEntry.domain_lookup_start - perfEntry.fetch_start > 0) {
+ performanceParts['app cache'] = {
+ start: perfEntry.fetch_start,
+ end: perfEntry.domain_lookup_start,
+ color: colorForSection('app cache'),
+ }
}
}
- if (perfEntry.domain_lookup_end && perfEntry.domain_lookup_start) {
- performanceParts['dns lookup'] = {
- start: perfEntry.domain_lookup_start,
- end: perfEntry.domain_lookup_end,
- color: colorForSection('dns lookup'),
+ if (isPresent(perfEntry.domain_lookup_end) && isPresent(perfEntry.domain_lookup_start)) {
+ if (perfEntry.domain_lookup_end - perfEntry.domain_lookup_start > 0) {
+ performanceParts['dns lookup'] = {
+ start: perfEntry.domain_lookup_start,
+ end: perfEntry.domain_lookup_end,
+ color: colorForSection('dns lookup'),
+ }
}
}
- if (perfEntry.connect_end && perfEntry.connect_start) {
- performanceParts['connection time'] = {
- start: perfEntry.connect_start,
- end: perfEntry.connect_end,
- color: colorForSection('connection time'),
- }
-
- if (perfEntry.secure_connection_start) {
- performanceParts['tls time'] = {
- start: perfEntry.secure_connection_start,
+ if (isPresent(perfEntry.connect_end) && isPresent(perfEntry.connect_start)) {
+ if (perfEntry.connect_end - perfEntry.connect_start > 0) {
+ performanceParts['connection time'] = {
+ start: perfEntry.connect_start,
end: perfEntry.connect_end,
- color: colorForSection('tls time'),
- reducedHeight: true,
+ color: colorForSection('connection time'),
+ }
+
+ if (isPresent(perfEntry.secure_connection_start) && perfEntry.secure_connection_start > 0) {
+ performanceParts['tls time'] = {
+ start: perfEntry.secure_connection_start,
+ end: perfEntry.connect_end,
+ color: colorForSection('tls time'),
+ reducedHeight: true,
+ }
}
}
}
- if (perfEntry.connect_end && perfEntry.request_start && perfEntry.connect_end !== perfEntry.request_start) {
- performanceParts['request queuing time'] = {
- start: perfEntry.connect_end,
- end: perfEntry.request_start,
- color: colorForSection('request queuing time'),
+ if (
+ isPresent(perfEntry.connect_end) &&
+ isPresent(perfEntry.request_start) &&
+ perfEntry.connect_end !== perfEntry.request_start
+ ) {
+ if (perfEntry.request_start - perfEntry.connect_end > 0) {
+ performanceParts['request queuing time'] = {
+ start: perfEntry.connect_end,
+ end: perfEntry.request_start,
+ color: colorForSection('request queuing time'),
+ }
}
}
- if (perfEntry.response_start && perfEntry.request_start) {
- performanceParts['waiting for first byte'] = {
- start: perfEntry.request_start,
- end: perfEntry.response_start,
- color: colorForSection('waiting for first byte'),
+ if (isPresent(perfEntry.response_start) && isPresent(perfEntry.request_start)) {
+ if (perfEntry.response_start - perfEntry.request_start > 0) {
+ performanceParts['waiting for first byte'] = {
+ start: perfEntry.request_start,
+ end: perfEntry.response_start,
+ color: colorForSection('waiting for first byte'),
+ }
}
}
- if (perfEntry.response_start && perfEntry.response_end) {
- performanceParts['receiving response'] = {
- start: perfEntry.response_start,
- end: perfEntry.response_end,
- color: colorForSection('receiving response'),
+ if (isPresent(perfEntry.response_start) && isPresent(perfEntry.response_end)) {
+ if (perfEntry.response_end - perfEntry.response_start > 0) {
+ // if loading from disk cache then response_start is 0 but fetch_start is not
+ let start = perfEntry.response_start
+ if (perfEntry.response_start === 0 && isPresent(perfEntry.fetch_start)) {
+ start = perfEntry.fetch_start
+ }
+ performanceParts['receiving response'] = {
+ start: start,
+ end: perfEntry.response_end,
+ color: colorForSection('receiving response'),
+ }
}
}
- if (perfEntry.response_end && perfEntry.load_event_end) {
- performanceParts['document processing'] = {
- start: perfEntry.response_end,
- end: perfEntry.load_event_end,
- color: colorForSection('document processing'),
+ if (isPresent(perfEntry.response_end) && isPresent(perfEntry.load_event_end)) {
+ if (perfEntry.load_event_end - perfEntry.response_end > 0) {
+ performanceParts['document processing'] = {
+ start: perfEntry.response_end,
+ end: perfEntry.load_event_end,
+ color: colorForSection('document processing'),
+ }
}
}
return performanceParts
}
+function percentage(partDuration: number, totalDuration: number, min: number): number {
+ return Math.min(Math.max(min, (partDuration / totalDuration) * 100), 100)
+}
+
function percentagesWithinEventRange({
partStart,
partEnd,
@@ -204,20 +234,20 @@ function percentagesWithinEventRange({
const partStartRelativeToTimeline = partStart - rangeStart
const partDuration = partEnd - partStart
- const partPercentage = Math.max(0.1, (partDuration / totalDuration) * 100) //less than 0.1% is not visible
- const partStartPercentage = (partStartRelativeToTimeline / totalDuration) * 100
+ const partPercentage = percentage(partDuration, totalDuration, 0.1)
+ const partStartPercentage = percentage(partStartRelativeToTimeline, totalDuration, 0)
return { startPercentage: `${partStartPercentage}%`, widthPercentage: `${partPercentage}%` }
}
-const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element => {
+const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element | null => {
const rangeStart = performanceEvent.start_time
- const rangeEnd = performanceEvent.response_end
+ const rangeEnd = performanceEvent.load_event_end ? performanceEvent.load_event_end : performanceEvent.response_end
if (typeof rangeStart === 'number' && typeof rangeEnd === 'number') {
- const performanceParts = calculatePerformanceParts(performanceEvent)
+ const timings = calculatePerformanceParts(performanceEvent)
return (
{perfSections.map((section) => {
- const matchedSection = performanceParts[section]
+ const matchedSection = timings[section]
const start = matchedSection?.start
const end = matchedSection?.end
const partDuration = end - start
@@ -264,7 +294,7 @@ const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent
)
}
- return Cannot render performance timeline for this request
+ return null
}
const TableView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element => {
@@ -284,11 +314,15 @@ export const NetworkRequestTiming = ({
}): JSX.Element | null => {
const [timelineMode, setTimelineMode] = useState(true)
+ // if timeline view renders null then we fall back to table view
+ const timelineView = timelineMode ? : null
+
return (
setTimelineMode(!timelineMode)}
data-attr={`switch-timing-to-${timelineMode ? 'table' : 'timeline'}-view`}
@@ -297,11 +331,11 @@ export const NetworkRequestTiming = ({
- {timelineMode ? (
-
- ) : (
-
- )}
+ {timelineMode && timelineView ? timelineView :
}
)
}
+
+function isPresent(x: number | undefined): x is number {
+ return typeof x === 'number'
+}
diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts
new file mode 100644
index 0000000000000..ceb129e8d8e9a
--- /dev/null
+++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts
@@ -0,0 +1,192 @@
+import { InitiatorType } from 'posthog-js'
+import { calculatePerformanceParts } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming'
+import { mapRRWebNetworkRequest } from 'scenes/session-recordings/player/inspector/performance-event-utils'
+
+jest.mock('lib/colors', () => {
+ return {
+ getSeriesColor: jest.fn(() => '#000000'),
+ }
+})
+
+describe('calculatePerformanceParts', () => {
+ it('can calculate TTFB', () => {
+ const perfEvent = {
+ connect_end: 9525.599999964237,
+ connect_start: 9525.599999964237,
+ decoded_body_size: 18260,
+ domain_lookup_end: 9525.599999964237,
+ domain_lookup_start: 9525.599999964237,
+ duration: 935.5,
+ encoded_body_size: 18260,
+ entry_type: 'resource',
+ fetch_start: 9525.599999964237,
+ initiator_type: 'fetch',
+ name: 'http://localhost:8000/api/organizations/@current/plugins/repository/',
+ next_hop_protocol: 'http/1.1',
+ redirect_end: 0,
+ redirect_start: 0,
+ render_blocking_status: 'non-blocking',
+ request_start: 9803.099999964237,
+ response_end: 10461.099999964237,
+ response_start: 10428.399999976158,
+ response_status: 200,
+ secure_connection_start: 0,
+ start_time: 9525.599999964237,
+ time_origin: '1699990397357',
+ timestamp: 1699990406882,
+ transfer_size: 18560,
+ window_id: '018bcf51-b1f0-7fe0-ac05-10543621f4f2',
+ worker_start: 0,
+ uuid: '12345',
+ distinct_id: '23456',
+ session_id: 'abcde',
+ pageview_id: 'fghij',
+ current_url: 'http://localhost:8000/insights',
+ }
+
+ expect(calculatePerformanceParts(perfEvent)).toEqual({
+ 'request queuing time': {
+ color: '#000000',
+ end: 9803.099999964237,
+ start: 9525.599999964237,
+ },
+
+ 'waiting for first byte': {
+ color: '#000000',
+ end: 10428.399999976158,
+ start: 9803.099999964237,
+ },
+ 'receiving response': {
+ color: '#000000',
+ end: 10461.099999964237,
+ start: 10428.399999976158,
+ },
+ })
+ })
+
+ it('can handle gravatar timings', () => {
+ const gravatarReqRes = {
+ name: 'https://www.gravatar.com/avatar/2e7d95b60efbe947f71009a1af1ba8d0?s=96&d=404',
+ entryType: 'resource',
+ initiatorType: 'fetch' as InitiatorType,
+ deliveryType: '',
+ nextHopProtocol: '',
+ renderBlockingStatus: 'non-blocking',
+ workerStart: 0,
+ redirectStart: 0,
+ redirectEnd: 0,
+ domainLookupStart: 0,
+ domainLookupEnd: 0,
+ connectStart: 0,
+ secureConnectionStart: 0,
+ connectEnd: 0,
+ requestStart: 0,
+ responseStart: 0,
+ firstInterimResponseStart: 0,
+ // only fetch start and response end
+ // and transfer size is 0
+ // loaded from disk cache
+ startTime: 18229,
+ fetchStart: 18228.5,
+ responseEnd: 18267.5,
+ endTime: 18268,
+ duration: 39,
+ transferSize: 0,
+ encodedBodySize: 0,
+ decodedBodySize: 0,
+ responseStatus: 200,
+ serverTiming: [],
+ timeOrigin: 1700296048424,
+ timestamp: 1700296066652,
+ method: 'GET',
+ status: 200,
+ requestHeaders: {},
+ requestBody: null,
+ responseHeaders: {
+ 'cache-control': 'max-age=300',
+ 'content-length': '13127',
+ 'content-type': 'image/png',
+ expires: 'Sat, 18 Nov 2023 08:32:46 GMT',
+ 'last-modified': 'Wed, 02 Feb 2022 09:11:05 GMT',
+ },
+ responseBody: '�PNGblah',
+ }
+ const mappedToPerfEvent = mapRRWebNetworkRequest(gravatarReqRes, 'windowId', 1700296066652)
+ expect(calculatePerformanceParts(mappedToPerfEvent)).toEqual({
+ // 'app cache' not included - end would be before beginning
+ // 'connection time' has 0 length
+ // 'dns lookup' has 0 length
+ // 'redirect has 0 length
+ // 'tls time' has 0 length
+ // TTFB has 0 length
+ 'receiving response': {
+ color: '#000000',
+ end: 18267.5,
+ start: 18228.5,
+ },
+ })
+ })
+
+ it('can handle no TLS connection timing', () => {
+ const tlsFreeReqRes = {
+ name: 'http://localhost:8000/decide/?v=3&ip=1&_=1700319068450&ver=1.91.1',
+ entryType: 'resource',
+ startTime: 6648,
+ duration: 93.40000003576279,
+ initiatorType: 'xmlhttprequest' as InitiatorType,
+ deliveryType: '',
+ nextHopProtocol: 'http/1.1',
+ renderBlockingStatus: 'non-blocking',
+ workerStart: 0,
+ redirectStart: 0,
+ redirectEnd: 0,
+ fetchStart: 6647.699999988079,
+ domainLookupStart: 6648.800000011921,
+ domainLookupEnd: 6648.800000011921,
+ connectStart: 6648.800000011921,
+ secureConnectionStart: 0,
+ connectEnd: 6649.300000011921,
+ requestStart: 6649.5,
+ responseStart: 6740.800000011921,
+ firstInterimResponseStart: 0,
+ responseEnd: 6741.100000023842,
+ transferSize: 2383,
+ encodedBodySize: 2083,
+ decodedBodySize: 2083,
+ responseStatus: 200,
+ serverTiming: [],
+ endTime: 6741,
+ timeOrigin: 1700319061802,
+ timestamp: 1700319068449,
+ isInitial: true,
+ }
+ const mappedToPerfEvent = mapRRWebNetworkRequest(tlsFreeReqRes, 'windowId', 1700319068449)
+ expect(calculatePerformanceParts(mappedToPerfEvent)).toEqual({
+ 'app cache': {
+ color: '#000000',
+ end: 6648.800000011921,
+ start: 6647.699999988079,
+ },
+ 'connection time': {
+ color: '#000000',
+ end: 6649.300000011921,
+ start: 6648.800000011921,
+ },
+ 'waiting for first byte': {
+ color: '#000000',
+ end: 6740.800000011921,
+ start: 6649.5,
+ },
+ 'receiving response': {
+ color: '#000000',
+ end: 6741.100000023842,
+ start: 6740.800000011921,
+ },
+ 'request queuing time': {
+ color: '#000000',
+ end: 6649.5,
+ start: 6649.300000011921,
+ },
+ })
+ })
+})
diff --git a/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts
index 003bda61eed07..502b85434f9e2 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts
+++ b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts
@@ -1,11 +1,10 @@
import { eventWithTime } from '@rrweb/types'
-import posthog from 'posthog-js'
+import { CapturedNetworkRequest } from 'posthog-js'
import { PerformanceEvent } from '~/types'
const NETWORK_PLUGIN_NAME = 'posthog/network@1'
const RRWEB_NETWORK_PLUGIN_NAME = 'rrweb/network@1'
-const IGNORED_POSTHOG_PATHS = ['/s/', '/e/', '/i/v0/e/']
export const PerformanceEventReverseMapping: { [key: number]: keyof PerformanceEvent } = {
// BASE_PERFORMANCE_EVENT_COLUMNS
@@ -59,8 +58,97 @@ export const PerformanceEventReverseMapping: { [key: number]: keyof PerformanceE
40: 'timestamp',
}
+export const RRWebPerformanceEventReverseMapping: Record = {
+ // BASE_PERFORMANCE_EVENT_COLUMNS
+ entryType: 'entry_type',
+ timeOrigin: 'time_origin',
+ name: 'name',
+
+ // RESOURCE_EVENT_COLUMNS
+ startTime: 'start_time',
+ redirectStart: 'redirect_start',
+ redirectEnd: 'redirect_end',
+ workerStart: 'worker_start',
+ fetchStart: 'fetch_start',
+ domainLookupStart: 'domain_lookup_start',
+ domainLookupEnd: 'domain_lookup_end',
+ connectStart: 'connect_start',
+ secureConnectionStart: 'secure_connection_start',
+ connectEnd: 'connect_end',
+ requestStart: 'request_start',
+ responseStart: 'response_start',
+ responseEnd: 'response_end',
+ decodedBodySize: 'decoded_body_size',
+ encodedBodySize: 'encoded_body_size',
+ initiatorType: 'initiator_type',
+ nextHopProtocol: 'next_hop_protocol',
+ renderBlockingStatus: 'render_blocking_status',
+ responseStatus: 'response_status',
+ transferSize: 'transfer_size',
+
+ // LARGEST_CONTENTFUL_PAINT_EVENT_COLUMNS
+ largestContentfulPaintElement: 'largest_contentful_paint_element',
+ largestContentfulPaintRenderTime: 'largest_contentful_paint_render_time',
+ largestContentfulPaintLoadTime: 'largest_contentful_paint_load_time',
+ largestContentfulPaintSize: 'largest_contentful_paint_size',
+ largestContentfulPaintId: 'largest_contentful_paint_id',
+ largestContentfulPaintUrl: 'largest_contentful_paint_url',
+
+ // NAVIGATION_EVENT_COLUMNS
+ domComplete: 'dom_complete',
+ domContentLoadedEvent: 'dom_content_loaded_event',
+ domInteractive: 'dom_interactive',
+ loadEventEnd: 'load_event_end',
+ loadEventStart: 'load_event_start',
+ redirectCount: 'redirect_count',
+ navigationType: 'navigation_type',
+ unloadEventEnd: 'unload_event_end',
+ unloadEventStart: 'unload_event_start',
+
+ // Added after v1
+ duration: 'duration',
+ timestamp: 'timestamp',
+
+ //rrweb/network@1
+ isInitial: 'is_initial',
+ requestHeaders: 'request_headers',
+ responseHeaders: 'response_headers',
+ requestBody: 'request_body',
+ responseBody: 'response_body',
+ method: 'method',
+}
+
+export function mapRRWebNetworkRequest(
+ capturedRequest: CapturedNetworkRequest,
+ windowId: string,
+ timestamp: PerformanceEvent['timestamp']
+): PerformanceEvent {
+ const data: Partial = {
+ timestamp: timestamp,
+ window_id: windowId,
+ raw: capturedRequest,
+ }
+
+ Object.entries(RRWebPerformanceEventReverseMapping).forEach(([key, value]) => {
+ if (key in capturedRequest) {
+ data[value] = capturedRequest[key]
+ }
+ })
+
+ // KLUDGE: this shouldn't be necessary but let's display correctly while we figure out why it is.
+ if (!data.name && 'url' in capturedRequest) {
+ data.name = capturedRequest.url as string | undefined
+ }
+
+ return data as PerformanceEvent
+}
+
export function matchNetworkEvents(snapshotsByWindowId: Record): PerformanceEvent[] {
- const eventsMapping: Record> = {}
+ // we only support rrweb/network@1 events or posthog/network@1 events in any one recording
+ // apart from during testing, where we might have both
+ // if we have both, we only display posthog/network@1 events
+ const events: PerformanceEvent[] = []
+ const rrwebEvents: PerformanceEvent[] = []
// we could do this in one pass, but it's easier to log missing events
// when we have all the posthog/network@1 events first
@@ -84,93 +172,27 @@ export function matchNetworkEvents(snapshotsByWindowId: Record {
- const snapshots = snapshotsByWindowId[1]
- snapshots.forEach((snapshot: eventWithTime) => {
if (
snapshot.type === 6 && // RRWeb plugin event type
snapshot.data.plugin === RRWEB_NETWORK_PLUGIN_NAME
) {
const payload = snapshot.data.payload as any
+
if (!Array.isArray(payload.requests) || payload.requests.length === 0) {
return
}
payload.requests.forEach((capturedRequest: any) => {
- const matchedURL = eventsMapping[capturedRequest.url]
-
- const matchedStartTime = matchedURL ? matchedURL[capturedRequest.startTime] : null
-
- if (matchedStartTime && matchedStartTime.length === 1) {
- matchedStartTime[0].response_status = capturedRequest.status
- matchedStartTime[0].request_headers = capturedRequest.requestHeaders
- matchedStartTime[0].request_body = capturedRequest.requestBody
- matchedStartTime[0].response_headers = capturedRequest.responseHeaders
- matchedStartTime[0].response_body = capturedRequest.responseBody
- matchedStartTime[0].method = capturedRequest.method
- } else if (matchedStartTime && matchedStartTime.length > 1) {
- // find in eventsMapping[capturedRequest.url][capturedRequest.startTime] by matching capturedRequest.endTime and element.response_end
- const matchedEndTime = matchedStartTime.find(
- (x) =>
- typeof x.response_end === 'number' &&
- Math.round(x.response_end) === capturedRequest.endTime
- )
- if (matchedEndTime) {
- matchedEndTime.response_status = capturedRequest.status
- matchedEndTime.request_headers = capturedRequest.requestHeaders
- matchedEndTime.request_body = capturedRequest.requestBody
- matchedEndTime.response_headers = capturedRequest.responseHeaders
- matchedEndTime.response_body = capturedRequest.responseBody
- matchedEndTime.method = capturedRequest.method
- } else {
- const capturedURL = new URL(capturedRequest.url)
- const capturedPath = capturedURL.pathname
-
- if (!IGNORED_POSTHOG_PATHS.some((x) => capturedPath === x)) {
- posthog.capture('Had matches but still could not match rrweb/network@1 event', {
- rrwebNetworkEvent: payload,
- possibleMatches: matchedStartTime,
- totalMatchedURLs: Object.keys(eventsMapping).length,
- })
- }
- }
- } else {
- const capturedURL = new URL(capturedRequest.url)
- const capturedPath = capturedURL.pathname
- if (!IGNORED_POSTHOG_PATHS.some((x) => capturedPath === x)) {
- posthog.capture('Could not match rrweb/network@1 event', {
- rrwebNetworkEvent: payload,
- possibleMatches: eventsMapping[capturedRequest.url],
- totalMatchedURLs: Object.keys(eventsMapping).length,
- })
- }
- }
+ const data: PerformanceEvent = mapRRWebNetworkRequest(capturedRequest, windowId, snapshot.timestamp)
+
+ rrwebEvents.push(data)
})
}
})
})
- // now flatten the eventsMapping into a single array
- return Object.values(eventsMapping).reduce((acc: PerformanceEvent[], eventsByURL) => {
- Object.values(eventsByURL).forEach((eventsByTime) => {
- acc.push(...eventsByTime)
- })
- return acc
- }, [])
+ return events.length ? events : rrwebEvents
}
diff --git a/frontend/src/scenes/surveys/Surveys.tsx b/frontend/src/scenes/surveys/Surveys.tsx
index e335f95398829..877895ccb38a2 100644
--- a/frontend/src/scenes/surveys/Surveys.tsx
+++ b/frontend/src/scenes/surveys/Surveys.tsx
@@ -119,7 +119,7 @@ export function Surveys(): JSX.Element {
]}
/>
-
+
{showSurveysDisabledBanner ? (
- {JSON.stringify(event.properties[surveyResponseField])}
+ {typeof event.properties[surveyResponseField] !== 'string'
+ ? JSON.stringify(event.properties[surveyResponseField])
+ : event.properties[surveyResponseField]}
}
export interface CurrentBillCycleType {
diff --git a/latest_migrations.manifest b/latest_migrations.manifest
index 7ad1758c3c617..27497f398a6fa 100644
--- a/latest_migrations.manifest
+++ b/latest_migrations.manifest
@@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name
ee: 0015_add_verified_properties
otp_static: 0002_throttling
otp_totp: 0002_auto_20190420_0723
-posthog: 0364_team_external_data_workspace_rows
+posthog: 0366_alter_action_created_by
sessions: 0001_initial
social_django: 0010_uid_db_index
two_factor: 0007_auto_20201201_1019
diff --git a/package.json b/package.json
index d4f28b98c6dd1..523099b130582 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,7 @@
"@medv/finder": "^2.1.0",
"@microlink/react-json-view": "^1.21.3",
"@monaco-editor/react": "4.4.6",
- "@posthog/icons": "0.4.1",
+ "@posthog/icons": "0.4.10",
"@posthog/plugin-scaffold": "^1.4.4",
"@react-hook/size": "^2.1.2",
"@rrweb/types": "^2.0.0-alpha.11",
@@ -136,7 +136,7 @@
"monaco-editor": "^0.39.0",
"papaparse": "^5.4.1",
"pmtiles": "^2.11.0",
- "posthog-js": "1.92.0",
+ "posthog-js": "1.92.1",
"posthog-js-lite": "2.0.0-alpha5",
"prettier": "^2.8.8",
"prop-types": "^15.7.2",
@@ -151,7 +151,7 @@
"react-dom": "^18.2.0",
"react-draggable": "^4.2.0",
"react-grid-layout": "^1.3.0",
- "react-intersection-observer": "^9.4.3",
+ "react-intersection-observer": "^9.5.3",
"react-markdown": "^5.0.3",
"react-modal": "^3.15.1",
"react-resizable": "^3.0.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ecd402d2394c0..49390592b0659 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -36,8 +36,8 @@ dependencies:
specifier: 4.4.6
version: 4.4.6(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0)
'@posthog/icons':
- specifier: 0.4.1
- version: 0.4.1(react-dom@18.2.0)(react@18.2.0)
+ specifier: 0.4.10
+ version: 0.4.10(react-dom@18.2.0)(react@18.2.0)
'@posthog/plugin-scaffold':
specifier: ^1.4.4
version: 1.4.4
@@ -216,8 +216,8 @@ dependencies:
specifier: ^2.11.0
version: 2.11.0
posthog-js:
- specifier: 1.92.0
- version: 1.92.0
+ specifier: 1.92.1
+ version: 1.92.1
posthog-js-lite:
specifier: 2.0.0-alpha5
version: 2.0.0-alpha5
@@ -261,8 +261,8 @@ dependencies:
specifier: ^1.3.0
version: 1.3.4(react-dom@18.2.0)(react@18.2.0)
react-intersection-observer:
- specifier: ^9.4.3
- version: 9.4.3(react@18.2.0)
+ specifier: ^9.5.3
+ version: 9.5.3(react@18.2.0)
react-markdown:
specifier: ^5.0.3
version: 5.0.3(@types/react@17.0.52)(react@18.2.0)
@@ -3425,8 +3425,8 @@ packages:
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
dev: false
- /@posthog/icons@0.4.1(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-sR7lDltjoAeExsOMZOZvCz8Z1rHbBqhZo8RABCCvx00MoBCUuydE1y2xpSoP5BVfMogY4ycDktnihw4ICUsb3Q==}
+ /@posthog/icons@0.4.10(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-92/pvHxVSWpNri8XoT9cfLfzf7RRvYGn8qMM6vUhMwkebBiurg8/oQHY1rZ0GcKLvCvzyAtgIr4o/N7ma9kWlQ==}
peerDependencies:
react: '>=16.14.0'
react-dom: '>=16.14.0'
@@ -5960,7 +5960,7 @@ packages:
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
dependencies:
'@types/connect': 3.4.38
- '@types/node': 18.11.9
+ '@types/node': 18.18.4
dev: true
/@types/chart.js@2.9.37:
@@ -5988,7 +5988,7 @@ packages:
/@types/connect@3.4.38:
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 18.18.4
dev: true
/@types/cookie@0.4.1:
@@ -6266,7 +6266,7 @@ packages:
/@types/express-serve-static-core@4.17.41:
resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 18.18.4
'@types/qs': 6.9.10
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@@ -6600,7 +6600,7 @@ packages:
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
dependencies:
'@types/mime': 1.3.5
- '@types/node': 18.11.9
+ '@types/node': 18.18.4
dev: true
/@types/serve-static@1.15.4:
@@ -6616,7 +6616,7 @@ packages:
dependencies:
'@types/http-errors': 2.0.4
'@types/mime': 3.0.4
- '@types/node': 18.11.9
+ '@types/node': 18.18.4
dev: true
/@types/set-cookie-parser@2.4.2:
@@ -15798,8 +15798,8 @@ packages:
resolution: {integrity: sha512-tlkBdypJuvK/s00n4EiQjwYVfuuZv6vt8BF3g1ooIQa2Gz9Vz80p8q3qsPLZ0V5ErGRy6i3Q4fWC9TDzR7GNRQ==}
dev: false
- /posthog-js@1.92.0:
- resolution: {integrity: sha512-87bZ/qwBbIqvkIV4YYn65oIPEsRcWihA3jX7WV33LvZWaU1InlE6cwj95SleIVLiND4Ofm+cKXZeWwcRnrXkKA==}
+ /posthog-js@1.92.1:
+ resolution: {integrity: sha512-xtuTfM/acfDauiEfIdKF6d911KUZQ7RLii2COAYEoPWr3cVUFoNUoRQz9QJvgDlV2j22Zwl+mnXacUeua+Yi1A==}
dependencies:
fflate: 0.4.8
dev: false
@@ -16849,8 +16849,8 @@ packages:
react: 18.2.0
dev: true
- /react-intersection-observer@9.4.3(react@18.2.0):
- resolution: {integrity: sha512-WNRqMQvKpupr6MzecAQI0Pj0+JQong307knLP4g/nBex7kYfIaZsPpXaIhKHR+oV8z+goUbH9e10j6lGRnTzlQ==}
+ /react-intersection-observer@9.5.3(react@18.2.0):
+ resolution: {integrity: sha512-NJzagSdUPS5rPhaLsHXYeJbsvdpbJwL6yCHtMk91hc0ufQ2BnXis+0QQ9NBh6n9n+Q3OyjR6OQLShYbaNBkThQ==}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
diff --git a/posthog/api/cohort.py b/posthog/api/cohort.py
index c525be8d263be..d417246b76d95 100644
--- a/posthog/api/cohort.py
+++ b/posthog/api/cohort.py
@@ -1,5 +1,15 @@
import csv
import json
+
+from django.db import DatabaseError
+import structlog
+
+from posthog.models.feature_flag.flag_matching import (
+ FeatureFlagMatcher,
+ FlagsMatcherCache,
+ get_feature_flag_hash_key_overrides,
+)
+from posthog.models.person.person import PersonDistinctId
from posthog.queries.insight import insight_sync_execute
import posthoganalytics
from posthog.metrics import LABEL_TEAM_ID
@@ -8,7 +18,7 @@
from typing import Any, Dict, cast
from django.conf import settings
-from django.db.models import QuerySet
+from django.db.models import QuerySet, Prefetch, prefetch_related_objects, OuterRef, Subquery
from django.db.models.expressions import F
from django.utils import timezone
from rest_framework import serializers, viewsets
@@ -67,6 +77,7 @@
from posthog.queries.util import get_earliest_timestamp
from posthog.tasks.calculate_cohort import (
calculate_cohort_from_list,
+ insert_cohort_from_feature_flag,
insert_cohort_from_insight_filter,
update_cohort,
)
@@ -80,6 +91,8 @@
labelnames=[LABEL_TEAM_ID],
)
+logger = structlog.get_logger(__name__)
+
class CohortSerializer(serializers.ModelSerializer):
created_by = UserBasicSerializer(read_only=True)
@@ -116,6 +129,8 @@ def _handle_static(self, cohort: Cohort, context: Dict) -> None:
request = self.context["request"]
if request.FILES.get("csv"):
self._calculate_static_by_csv(request.FILES["csv"], cohort)
+ elif context.get("from_feature_flag_key"):
+ insert_cohort_from_feature_flag.delay(cohort.pk, context["from_feature_flag_key"], self.context["team_id"])
else:
filter_data = request.GET.dict()
existing_cohort_id = context.get("from_cohort_id")
@@ -539,3 +554,109 @@ def insert_actors_into_cohort_by_query(cohort: Cohort, query: str, params: Dict[
cohort.errors_calculating = F("errors_calculating") + 1
cohort.save(update_fields=["errors_calculating", "is_calculating"])
capture_exception(err)
+
+
+def get_cohort_actors_for_feature_flag(cohort_id: int, flag: str, team_id: int, batchsize: int = 5_000):
+ # :TODO: Find a way to incorporate this into the same code path as feature flag evaluation
+ try:
+ feature_flag = FeatureFlag.objects.get(team_id=team_id, key=flag)
+ except FeatureFlag.DoesNotExist:
+ return []
+
+ if not feature_flag.active or feature_flag.deleted or feature_flag.aggregation_group_type_index is not None:
+ return []
+
+ cohort = Cohort.objects.get(pk=cohort_id)
+ matcher_cache = FlagsMatcherCache(team_id)
+ uuids_to_add_to_cohort = []
+ cohorts_cache = {}
+
+ if feature_flag.uses_cohorts:
+ cohorts_cache = {cohort.pk: cohort for cohort in Cohort.objects.filter(team_id=team_id, deleted=False)}
+
+ default_person_properties = {}
+ for condition in feature_flag.conditions:
+ property_list = Filter(data=condition).property_groups.flat
+ for property in property_list:
+ if property.operator not in ("is_set", "is_not_set") and property.type == "person":
+ default_person_properties[property.key] = ""
+
+ 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")
+ # get batchsize number of people at a time
+ start = 0
+ batch_of_persons = queryset[start : start + batchsize]
+ while batch_of_persons:
+ # TODO: Check if this subquery bulk fetch limiting is better than just doing a join for all distinct ids
+ # OR, if row by row getting single distinct id is better
+ # distinct_id = PersonDistinctId.objects.filter(person=person, team_id=team_id).values_list(
+ # "distinct_id", flat=True
+ # )[0]
+ distinct_id_subquery = Subquery(
+ PersonDistinctId.objects.filter(person_id=OuterRef("person_id")).values_list("id", flat=True)[:1]
+ )
+ prefetch_related_objects(
+ batch_of_persons,
+ Prefetch(
+ "persondistinctid_set",
+ to_attr="distinct_ids_cache",
+ queryset=PersonDistinctId.objects.filter(id__in=distinct_id_subquery),
+ ),
+ )
+
+ all_persons = list(batch_of_persons)
+ if len(all_persons) == 0:
+ break
+
+ for person in all_persons:
+ 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 = []
+
+ start += batchsize
+ batch_of_persons = queryset[start : start + batchsize]
+
+ if len(uuids_to_add_to_cohort) > 0:
+ cohort.insert_users_list_by_uuid(uuids_to_add_to_cohort, insert_in_clickhouse=True, batchsize=batchsize)
+
+ except Exception as err:
+ if settings.DEBUG or settings.TEST:
+ raise err
+ capture_exception(err)
diff --git a/posthog/api/feature_flag.py b/posthog/api/feature_flag.py
index 45bd8e077de1f..92add84a0bcab 100644
--- a/posthog/api/feature_flag.py
+++ b/posthog/api/feature_flag.py
@@ -1,5 +1,6 @@
import json
from typing import Any, Dict, List, Optional, cast
+from datetime import datetime
from django.db.models import QuerySet, Q, deletion
from django.conf import settings
@@ -16,6 +17,7 @@
from rest_framework.request import Request
from rest_framework.response import Response
from sentry_sdk import capture_exception
+from posthog.api.cohort import CohortSerializer
from posthog.api.forbid_destroy_model import ForbidDestroyModel
from posthog.api.routing import StructuredViewSetMixin
@@ -625,6 +627,28 @@ def user_blast_radius(self, request: request.Request, **kwargs):
}
)
+ @action(methods=["POST"], detail=True)
+ def create_static_cohort_for_flag(self, request: request.Request, **kwargs):
+ feature_flag = self.get_object()
+ feature_flag_key = feature_flag.key
+ cohort_serializer = CohortSerializer(
+ data={
+ "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")}',
+ },
+ context={
+ "request": request,
+ "team": self.team,
+ "team_id": self.team_id,
+ "from_feature_flag_key": feature_flag_key,
+ },
+ )
+
+ cohort_serializer.is_valid(raise_exception=True)
+ cohort_serializer.save()
+ return Response({"cohort": cohort_serializer.data}, status=201)
+
@action(methods=["GET"], url_path="activity", detail=False)
def all_activity(self, request: request.Request, **kwargs):
limit = int(request.query_params.get("limit", "10"))
diff --git a/posthog/api/organization_feature_flag.py b/posthog/api/organization_feature_flag.py
index 4f7d9fd6f78e2..44648bd2cd0f2 100644
--- a/posthog/api/organization_feature_flag.py
+++ b/posthog/api/organization_feature_flag.py
@@ -12,6 +12,7 @@
from posthog.api.routing import StructuredViewSetMixin
from posthog.api.feature_flag import FeatureFlagSerializer
from posthog.api.feature_flag import CanEditFeatureFlag
+from posthog.api.shared import UserBasicSerializer
from posthog.models import FeatureFlag, Team
from posthog.models.cohort import Cohort
from posthog.models.filters.filter import Filter
@@ -44,15 +45,10 @@ def retrieve(self, request, *args, **kwargs):
{
"flag_id": flag.id,
"team_id": flag.team_id,
- "created_by": {
- "id": flag.created_by.id,
- "uuid": flag.created_by.uuid,
- "distinct_id": flag.created_by.distinct_id,
- "first_name": flag.created_by.first_name,
- "email": flag.created_by.email,
- "is_email_verified": flag.created_by.is_email_verified,
- },
- "filters": flag.filters,
+ "created_by": UserBasicSerializer(flag.created_by).data
+ if hasattr(flag, "created_by") and flag.created_by
+ else None,
+ "filters": flag.get_filters(),
"created_at": flag.created_at,
"active": flag.active,
}
@@ -168,7 +164,7 @@ def copy_flags(self, request, *args, **kwargs):
flag_data = {
"key": flag_to_copy.key,
"name": flag_to_copy.name,
- "filters": flag_to_copy.filters,
+ "filters": flag_to_copy.get_filters(),
"active": flag_to_copy.active,
"rollout_percentage": flag_to_copy.rollout_percentage,
"ensure_experience_continuity": flag_to_copy.ensure_experience_continuity,
diff --git a/posthog/api/search.py b/posthog/api/search.py
index a48f716f902f7..cbdd898949fd1 100644
--- a/posthog/api/search.py
+++ b/posthog/api/search.py
@@ -21,8 +21,8 @@
ENTITY_MAP = {
"insight": {
"klass": Insight,
- "search_fields": {"name": "A", "derived_name": "B", "description": "C"},
- "extra_fields": ["name", "derived_name", "description"],
+ "search_fields": {"name": "A", "description": "C"},
+ "extra_fields": ["name", "description", "filters", "query"],
},
"dashboard": {
"klass": Dashboard,
diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr
index 6fc6c63c17f20..c7922b68432eb 100644
--- a/posthog/api/test/__snapshots__/test_feature_flag.ambr
+++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr
@@ -334,6 +334,1559 @@
HAVING max(is_deleted) = 0 SETTINGS optimize_aggregation_in_order = 1)
'
---
+# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator
+ '
+ 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.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_iterator.10
+ '
+ 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 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
+ '
+ 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 1)
+ AND "posthog_persondistinctid"."person_id" IN (1,
+ 2,
+ 3,
+ 4,
+ 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
+ '
+ 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.17
+ '
+ 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_iterator.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
+ ORDER BY "posthog_person"."id" ASC
+ LIMIT 2
+ '
+---
+# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.3
+ '
+ 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 1)
+ AND "posthog_persondistinctid"."person_id" IN (1,
+ 2,
+ 3,
+ 4,
+ 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
+ '
+ 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 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 1)
+ AND "posthog_persondistinctid"."person_id" IN (1,
+ 2,
+ 3,
+ 4,
+ 5 /* ... */))
+ '
+---
+# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_iterator.9
+ '
+ 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_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",
+ "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_experience_continuity_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_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
+ '
+ 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 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
+ ORDER BY "posthog_person"."id" ASC
+ LIMIT 5000
+ '
+---
+# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.3
+ '
+ 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 1)
+ AND "posthog_persondistinctid"."person_id" IN (1,
+ 2,
+ 3,
+ 4,
+ 5 /* ... */))
+ '
+---
+# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.4
+ '
+ SELECT "posthog_featureflaghashkeyoverride"."feature_flag_key",
+ "posthog_featureflaghashkeyoverride"."hash_key",
+ "posthog_featureflaghashkeyoverride"."person_id"
+ FROM "posthog_featureflaghashkeyoverride"
+ WHERE ("posthog_featureflaghashkeyoverride"."person_id" IN (1,
+ 2,
+ 3,
+ 4,
+ 5 /* ... */)
+ AND "posthog_featureflaghashkeyoverride"."team_id" = 2)
+ '
+---
+# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.5
+ '
+ SELECT "posthog_featureflaghashkeyoverride"."feature_flag_key",
+ "posthog_featureflaghashkeyoverride"."hash_key",
+ "posthog_featureflaghashkeyoverride"."person_id"
+ FROM "posthog_featureflaghashkeyoverride"
+ WHERE ("posthog_featureflaghashkeyoverride"."person_id" IN (1,
+ 2,
+ 3,
+ 4,
+ 5 /* ... */)
+ AND "posthog_featureflaghashkeyoverride"."team_id" = 2)
+ '
+---
+# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.6
+ '
+ SELECT "posthog_featureflaghashkeyoverride"."feature_flag_key",
+ "posthog_featureflaghashkeyoverride"."hash_key",
+ "posthog_featureflaghashkeyoverride"."person_id"
+ FROM "posthog_featureflaghashkeyoverride"
+ WHERE ("posthog_featureflaghashkeyoverride"."person_id" IN (1,
+ 2,
+ 3,
+ 4,
+ 5 /* ... */)
+ AND "posthog_featureflaghashkeyoverride"."team_id" = 2)
+ '
+---
+# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.7
+ '
+ 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 5000
+ OFFSET 5000
+ '
+---
+# name: TestCohortGenerationForFeatureFlag.test_creating_static_cohort_with_experience_continuity_flag.8
+ '
+ 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.9
+ '
+ 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_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",
+ "posthog_user"."password",
+ "posthog_user"."last_login",
+ "posthog_user"."first_name",
+ "posthog_user"."last_name",
+ "posthog_user"."is_staff",
+ "posthog_user"."is_active",
+ "posthog_user"."date_joined",
+ "posthog_user"."uuid",
+ "posthog_user"."current_organization_id",
+ "posthog_user"."current_team_id",
+ "posthog_user"."email",
+ "posthog_user"."pending_email",
+ "posthog_user"."temporary_token",
+ "posthog_user"."distinct_id",
+ "posthog_user"."is_email_verified",
+ "posthog_user"."has_seen_product_intro_for",
+ "posthog_user"."email_opt_in",
+ "posthog_user"."partial_notification_settings",
+ "posthog_user"."anonymize_data",
+ "posthog_user"."toolbar_mode",
+ "posthog_user"."events_column_config"
+ FROM "posthog_user"
+ WHERE "posthog_user"."id" = 2
+ LIMIT 21 /**/
+ '
+---
+# name: TestFeatureFlag.test_creating_static_cohort.1
+ '
+ 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"."external_data_workspace_id",
+ "posthog_team"."external_data_workspace_last_synced_at"
+ FROM "posthog_team"
+ WHERE "posthog_team"."id" = 2
+ LIMIT 21 /*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.10
+ '
+ 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 1)
+ AND "posthog_persondistinctid"."person_id" IN (1,
+ 2,
+ 3,
+ 4,
+ 5 /* ... */)) /*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.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 5000
+ OFFSET 5000 /*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
+ '
+ 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))) /*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.13
+ '
+ 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 /*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.14
+ '
+ /* user_id:189 celery:posthog.tasks.calculate_cohort.insert_cohort_from_feature_flag */
+ SELECT count(DISTINCT person_id)
+ FROM person_static_cohort
+ WHERE team_id = 2
+ AND cohort_id = 2
+ '
+---
+# name: TestFeatureFlag.test_creating_static_cohort.15
+ '
+ /* user_id:0 request:_snapshot_ */
+ SELECT id
+ FROM person
+ INNER JOIN
+ (SELECT person_id
+ FROM person_static_cohort
+ WHERE team_id = 2
+ AND cohort_id = 2
+ GROUP BY person_id,
+ cohort_id,
+ team_id) cohort_persons ON cohort_persons.person_id = person.id
+ WHERE team_id = 2
+ GROUP BY id
+ HAVING max(is_deleted) = 0
+ ORDER BY argMax(person.created_at, version) DESC, id DESC
+ LIMIT 100 SETTINGS optimize_aggregation_in_order = 1
+ '
+---
+# name: TestFeatureFlag.test_creating_static_cohort.16
+ '
+ /* user_id:0 request:_snapshot_ */
+ SELECT id
+ FROM person
+ INNER JOIN
+ (SELECT person_id
+ FROM person_static_cohort
+ WHERE team_id = 2
+ AND cohort_id = 2
+ GROUP BY person_id,
+ cohort_id,
+ team_id) cohort_persons ON cohort_persons.person_id = person.id
+ WHERE team_id = 2
+ GROUP BY id
+ HAVING max(is_deleted) = 0
+ ORDER BY argMax(person.created_at, version) DESC, id DESC
+ LIMIT 100 SETTINGS optimize_aggregation_in_order = 1
+ '
+---
+# name: TestFeatureFlag.test_creating_static_cohort.17
+ '
+ SELECT "posthog_persondistinctid"."person_id",
+ "posthog_persondistinctid"."distinct_id"
+ FROM "posthog_persondistinctid"
+ WHERE ("posthog_persondistinctid"."distinct_id" IN ('person3')
+ AND "posthog_persondistinctid"."team_id" = 2) /*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.18
+ '
+ SELECT "posthog_featureflaghashkeyoverride"."feature_flag_key",
+ "posthog_featureflaghashkeyoverride"."hash_key",
+ "posthog_featureflaghashkeyoverride"."person_id"
+ FROM "posthog_featureflaghashkeyoverride"
+ WHERE ("posthog_featureflaghashkeyoverride"."person_id" IN (1,
+ 2,
+ 3,
+ 4,
+ 5 /* ... */)
+ AND "posthog_featureflaghashkeyoverride"."team_id" = 2) /*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.19
+ '
+ 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))) /*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.2
+ '
+ SELECT "posthog_organizationmembership"."id",
+ "posthog_organizationmembership"."organization_id",
+ "posthog_organizationmembership"."user_id",
+ "posthog_organizationmembership"."level",
+ "posthog_organizationmembership"."joined_at",
+ "posthog_organizationmembership"."updated_at",
+ "posthog_organization"."id",
+ "posthog_organization"."name",
+ "posthog_organization"."slug",
+ "posthog_organization"."created_at",
+ "posthog_organization"."updated_at",
+ "posthog_organization"."plugins_access_level",
+ "posthog_organization"."for_internal_metrics",
+ "posthog_organization"."is_member_join_email_enabled",
+ "posthog_organization"."enforce_2fa",
+ "posthog_organization"."customer_id",
+ "posthog_organization"."available_product_features",
+ "posthog_organization"."usage",
+ "posthog_organization"."never_drop_data",
+ "posthog_organization"."setup_section_2_completed",
+ "posthog_organization"."personalization",
+ "posthog_organization"."domain_whitelist",
+ "posthog_organization"."available_features"
+ FROM "posthog_organizationmembership"
+ INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
+ WHERE "posthog_organizationmembership"."user_id" = 2 /*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.20
+ '
+ 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 /*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.3
+ '
+ SELECT "posthog_instancesetting"."id",
+ "posthog_instancesetting"."key",
+ "posthog_instancesetting"."raw_value"
+ FROM "posthog_instancesetting"
+ WHERE "posthog_instancesetting"."key" = 'constance:posthog:RATE_LIMIT_ENABLED'
+ ORDER BY "posthog_instancesetting"."id" ASC
+ LIMIT 1 /*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.4
+ '
+ SELECT "posthog_organization"."id",
+ "posthog_organization"."name",
+ "posthog_organization"."slug",
+ "posthog_organization"."created_at",
+ "posthog_organization"."updated_at",
+ "posthog_organization"."plugins_access_level",
+ "posthog_organization"."for_internal_metrics",
+ "posthog_organization"."is_member_join_email_enabled",
+ "posthog_organization"."enforce_2fa",
+ "posthog_organization"."customer_id",
+ "posthog_organization"."available_product_features",
+ "posthog_organization"."usage",
+ "posthog_organization"."never_drop_data",
+ "posthog_organization"."setup_section_2_completed",
+ "posthog_organization"."personalization",
+ "posthog_organization"."domain_whitelist",
+ "posthog_organization"."available_features"
+ FROM "posthog_organization"
+ WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
+ LIMIT 21 /*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.5
+ '
+ 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",
+ "posthog_user"."id",
+ "posthog_user"."password",
+ "posthog_user"."last_login",
+ "posthog_user"."first_name",
+ "posthog_user"."last_name",
+ "posthog_user"."is_staff",
+ "posthog_user"."is_active",
+ "posthog_user"."date_joined",
+ "posthog_user"."uuid",
+ "posthog_user"."current_organization_id",
+ "posthog_user"."current_team_id",
+ "posthog_user"."email",
+ "posthog_user"."pending_email",
+ "posthog_user"."temporary_token",
+ "posthog_user"."distinct_id",
+ "posthog_user"."is_email_verified",
+ "posthog_user"."requested_password_reset_at",
+ "posthog_user"."has_seen_product_intro_for",
+ "posthog_user"."email_opt_in",
+ "posthog_user"."partial_notification_settings",
+ "posthog_user"."anonymize_data",
+ "posthog_user"."toolbar_mode",
+ "posthog_user"."events_column_config"
+ FROM "posthog_featureflag"
+ LEFT OUTER JOIN "posthog_user" ON ("posthog_featureflag"."created_by_id" = "posthog_user"."id")
+ WHERE ("posthog_featureflag"."team_id" = 2
+ AND "posthog_featureflag"."id" = 2)
+ LIMIT 21 /*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.6
+ '
+ 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 /*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.7
+ '
+ 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 /*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.8
+ '
+ 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 /*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.9
+ '
+ 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 5000 /*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
'
WITH target_person_ids AS
diff --git a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr
index e2b852a604b20..9916b9afadbbf 100644
--- a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr
+++ b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr
@@ -1440,8 +1440,8 @@
"posthog_experiment"."filters",
"posthog_experiment"."parameters",
"posthog_experiment"."secondary_metrics",
- "posthog_experiment"."feature_flag_id",
"posthog_experiment"."created_by_id",
+ "posthog_experiment"."feature_flag_id",
"posthog_experiment"."start_date",
"posthog_experiment"."end_date",
"posthog_experiment"."created_at",
diff --git a/posthog/api/test/batch_exports/test_delete.py b/posthog/api/test/batch_exports/test_delete.py
index 20375cecbb768..cc07ed4675151 100644
--- a/posthog/api/test/batch_exports/test_delete.py
+++ b/posthog/api/test/batch_exports/test_delete.py
@@ -241,3 +241,48 @@ def test_deletes_are_partitioned_by_team_id(client: HttpClient):
# Make sure we can still get the export with the right user
response = get_batch_export(client, team.pk, batch_export_id)
assert response.status_code == status.HTTP_200_OK
+
+
+@pytest.mark.django_db(transaction=True)
+def test_delete_batch_export_even_without_underlying_schedule(client: HttpClient):
+ """Test deleting a BatchExport completes even if underlying Schedule was already deleted."""
+ temporal = sync_connect()
+
+ destination_data = {
+ "type": "S3",
+ "config": {
+ "bucket_name": "my-production-s3-bucket",
+ "region": "us-east-1",
+ "prefix": "posthog-events/",
+ "aws_access_key_id": "abc123",
+ "aws_secret_access_key": "secret",
+ },
+ }
+ batch_export_data = {
+ "name": "my-production-s3-bucket-destination",
+ "destination": destination_data,
+ "interval": "hour",
+ }
+
+ organization = create_organization("Test Org")
+ team = create_team(organization)
+ user = create_user("test@user.com", "Test User", organization)
+ client.force_login(user)
+
+ with start_test_worker(temporal):
+ batch_export = create_batch_export_ok(client, team.pk, batch_export_data)
+ batch_export_id = batch_export["id"]
+
+ handle = temporal.get_schedule_handle(batch_export_id)
+ async_to_sync(handle.delete)()
+
+ with pytest.raises(RPCError):
+ describe_schedule(temporal, batch_export_id)
+
+ delete_batch_export_ok(client, team.pk, batch_export_id)
+
+ response = get_batch_export(client, team.pk, batch_export_id)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ with pytest.raises(RPCError):
+ describe_schedule(temporal, batch_export_id)
diff --git a/posthog/api/test/test_feature_flag.py b/posthog/api/test/test_feature_flag.py
index 0c6581a389561..06997913184f8 100644
--- a/posthog/api/test/test_feature_flag.py
+++ b/posthog/api/test/test_feature_flag.py
@@ -12,6 +12,7 @@
from freezegun.api import freeze_time
from rest_framework import status
from posthog import redis
+from posthog.api.cohort import get_cohort_actors_for_feature_flag
from posthog.api.feature_flag import FeatureFlagSerializer
from posthog.constants import AvailableFeature
@@ -23,6 +24,7 @@
FeatureFlagDashboards,
)
from posthog.models.dashboard import Dashboard
+from posthog.models.feature_flag.feature_flag import FeatureFlagHashKeyOverride
from posthog.models.group.util import create_group
from posthog.models.organization import Organization
from posthog.models.person import Person
@@ -34,6 +36,7 @@
ClickhouseTestMixin,
QueryMatchingTest,
_create_person,
+ flush_persons_and_events,
snapshot_clickhouse_queries,
snapshot_postgres_queries_context,
FuzzyInt,
@@ -41,7 +44,7 @@
from posthog.test.db_context_capturing import capture_db_queries
-class TestFeatureFlag(APIBaseTest):
+class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
feature_flag: FeatureFlag = None # type: ignore
maxDiff = None
@@ -1171,6 +1174,34 @@ def test_getting_flags_is_not_nplus1(self) -> None:
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
+ def test_getting_flags_with_no_creator(self) -> None:
+ FeatureFlag.objects.all().delete()
+
+ self.client.post(
+ f"/api/projects/{self.team.id}/feature_flags/",
+ data={
+ "name": f"flag",
+ "key": f"flag_0",
+ "filters": {"groups": [{"rollout_percentage": 5}]},
+ },
+ format="json",
+ ).json()
+
+ FeatureFlag.objects.create(
+ created_by=None,
+ team=self.team,
+ key="flag_role_access",
+ name="Flag role access",
+ )
+
+ with self.assertNumQueries(FuzzyInt(11, 12)):
+ response = self.client.get(f"/api/projects/{self.team.id}/feature_flags")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.json()["results"]), 2)
+ sorted_results = sorted(response.json()["results"], key=lambda x: x["key"])
+ self.assertEqual(sorted_results[1]["created_by"], None)
+ self.assertEqual(sorted_results[1]["key"], "flag_role_access")
+
@patch("posthog.api.feature_flag.report_user_action")
def test_my_flags(self, mock_capture):
self.client.post(
@@ -3539,6 +3570,309 @@ def test_feature_flag_dashboard_already_exists(self):
self.assertEquals(len(response_json["analytics_dashboards"]), 1)
+ @freeze_time("2021-01-01")
+ @snapshot_clickhouse_queries
+ def test_creating_static_cohort(self):
+ flag = FeatureFlag.objects.create(
+ team=self.team,
+ rollout_percentage=100,
+ filters={
+ "groups": [{"properties": [{"key": "key", "value": "value", "type": "person"}]}],
+ "multivariate": None,
+ },
+ name="some feature",
+ key="some-feature",
+ created_by=self.user,
+ )
+
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person1"],
+ properties={"key": "value"},
+ )
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person2"],
+ properties={"key": "value2"},
+ )
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person3"],
+ properties={"key2": "value3"},
+ )
+ flush_persons_and_events()
+
+ with snapshot_postgres_queries_context(self), self.settings(
+ CELERY_TASK_ALWAYS_EAGER=True, PERSON_ON_EVENTS_OVERRIDE=False, PERSON_ON_EVENTS_V2_OVERRIDE=False
+ ):
+ response = self.client.post(
+ f"/api/projects/{self.team.id}/feature_flags/{flag.id}/create_static_cohort_for_flag",
+ {},
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ # fires an async task for computation, but celery runs sync in tests
+ cohort_id = response.json()["cohort"]["id"]
+ cohort = Cohort.objects.get(id=cohort_id)
+ self.assertEqual(cohort.name, "Users with feature flag some-feature enabled at 2021-01-01 00:00:00")
+ self.assertEqual(cohort.count, 1)
+
+ response = self.client.get(f"/api/cohort/{cohort.pk}/persons")
+ self.assertEqual(len(response.json()["results"]), 1, response)
+
+
+class TestCohortGenerationForFeatureFlag(APIBaseTest, ClickhouseTestMixin):
+ def test_creating_static_cohort_with_deleted_flag(self):
+ FeatureFlag.objects.create(
+ team=self.team,
+ rollout_percentage=100,
+ filters={
+ "groups": [{"properties": [{"key": "key", "value": "value", "type": "person"}]}],
+ "multivariate": None,
+ },
+ name="some feature",
+ key="some-feature",
+ created_by=self.user,
+ deleted=True,
+ )
+
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person1"],
+ properties={"key": "value"},
+ )
+ flush_persons_and_events()
+
+ cohort = Cohort.objects.create(
+ team=self.team,
+ is_static=True,
+ name="some cohort",
+ )
+
+ with self.assertNumQueries(1):
+ get_cohort_actors_for_feature_flag(cohort.pk, "some-feature", self.team.pk)
+
+ cohort.refresh_from_db()
+ self.assertEqual(cohort.name, "some cohort")
+ # don't even try inserting anything, because invalid flag, so None instead of 0
+ self.assertEqual(cohort.count, None)
+
+ response = self.client.get(f"/api/cohort/{cohort.pk}/persons")
+ self.assertEqual(len(response.json()["results"]), 0, response)
+
+ def test_creating_static_cohort_with_inactive_flag(self):
+ FeatureFlag.objects.create(
+ team=self.team,
+ rollout_percentage=100,
+ filters={
+ "groups": [{"properties": [{"key": "key", "value": "value", "type": "person"}]}],
+ "multivariate": None,
+ },
+ name="some feature",
+ key="some-feature2",
+ created_by=self.user,
+ active=False,
+ )
+
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person1"],
+ properties={"key": "value"},
+ )
+ flush_persons_and_events()
+
+ cohort = Cohort.objects.create(
+ team=self.team,
+ is_static=True,
+ name="some cohort",
+ )
+
+ with self.assertNumQueries(1):
+ get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk)
+
+ cohort.refresh_from_db()
+ self.assertEqual(cohort.name, "some cohort")
+ # don't even try inserting anything, because invalid flag, so None instead of 0
+ self.assertEqual(cohort.count, None)
+
+ response = self.client.get(f"/api/cohort/{cohort.pk}/persons")
+ self.assertEqual(len(response.json()["results"]), 0, response)
+
+ @freeze_time("2021-01-01")
+ def test_creating_static_cohort_with_group_flag(self):
+ FeatureFlag.objects.create(
+ team=self.team,
+ rollout_percentage=100,
+ filters={
+ "groups": [{"properties": [{"key": "key", "value": "value", "type": "group", "group_type_index": 1}]}],
+ "multivariate": None,
+ "aggregation_group_type_index": 1,
+ },
+ name="some feature",
+ key="some-feature3",
+ created_by=self.user,
+ )
+
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person1"],
+ properties={"key": "value"},
+ )
+ flush_persons_and_events()
+
+ cohort = Cohort.objects.create(
+ team=self.team,
+ is_static=True,
+ name="some cohort",
+ )
+
+ with self.assertNumQueries(1):
+ get_cohort_actors_for_feature_flag(cohort.pk, "some-feature3", self.team.pk)
+
+ cohort.refresh_from_db()
+ self.assertEqual(cohort.name, "some cohort")
+ # don't even try inserting anything, because invalid flag, so None instead of 0
+ self.assertEqual(cohort.count, None)
+
+ response = self.client.get(f"/api/cohort/{cohort.pk}/persons")
+ self.assertEqual(len(response.json()["results"]), 0, response)
+
+ def test_creating_static_cohort_with_non_existing_flag(self):
+ cohort = Cohort.objects.create(
+ team=self.team,
+ is_static=True,
+ name="some cohort",
+ )
+
+ with self.assertNumQueries(1):
+ get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk)
+
+ cohort.refresh_from_db()
+ self.assertEqual(cohort.name, "some cohort")
+ # don't even try inserting anything, because invalid flag, so None instead of 0
+ self.assertEqual(cohort.count, None)
+
+ response = self.client.get(f"/api/cohort/{cohort.pk}/persons")
+ self.assertEqual(len(response.json()["results"]), 0, response)
+
+ def test_creating_static_cohort_with_experience_continuity_flag(self):
+ FeatureFlag.objects.create(
+ team=self.team,
+ filters={
+ "groups": [
+ {"properties": [{"key": "key", "value": "value", "type": "person"}], "rollout_percentage": 50}
+ ],
+ "multivariate": None,
+ },
+ name="some feature",
+ key="some-feature2",
+ created_by=self.user,
+ ensure_experience_continuity=True,
+ )
+
+ p1 = _create_person(team=self.team, distinct_ids=[f"person1"], properties={"key": "value"}, immediate=True)
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person2"],
+ properties={"key": "value"},
+ )
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person3"],
+ properties={"key": "value"},
+ )
+ flush_persons_and_events()
+ # TODO: Check right person is added here
+
+ FeatureFlagHashKeyOverride.objects.create(
+ feature_flag_key="some-feature2",
+ person=p1,
+ team=self.team,
+ hash_key="123",
+ )
+
+ cohort = Cohort.objects.create(
+ team=self.team,
+ is_static=True,
+ name="some cohort",
+ )
+
+ # TODO: Ensure server-side cursors are disabled, since in production we use this with pgbouncer
+ with snapshot_postgres_queries_context(self), self.assertNumQueries(12):
+ get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk)
+
+ cohort.refresh_from_db()
+ self.assertEqual(cohort.name, "some cohort")
+ self.assertEqual(cohort.count, 1)
+
+ response = self.client.get(f"/api/cohort/{cohort.pk}/persons")
+ self.assertEqual(len(response.json()["results"]), 1, response)
+
+ def test_creating_static_cohort_iterator(self):
+ FeatureFlag.objects.create(
+ team=self.team,
+ filters={
+ "groups": [
+ {"properties": [{"key": "key", "value": "value", "type": "person"}], "rollout_percentage": 100}
+ ],
+ "multivariate": None,
+ },
+ name="some feature",
+ key="some-feature2",
+ created_by=self.user,
+ )
+
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person1"],
+ properties={"key": "value"},
+ )
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person2"],
+ properties={"key": "value"},
+ )
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person3"],
+ properties={"key": "value"},
+ )
+ _create_person(
+ team=self.team,
+ distinct_ids=[f"person4"],
+ properties={"key": "valuu3"},
+ )
+ flush_persons_and_events()
+
+ cohort = Cohort.objects.create(
+ team=self.team,
+ is_static=True,
+ name="some cohort",
+ )
+
+ # Extra queries because each batch adds its own queries
+ with snapshot_postgres_queries_context(self), self.assertNumQueries(17):
+ get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk, batchsize=2)
+
+ cohort.refresh_from_db()
+ self.assertEqual(cohort.name, "some cohort")
+ self.assertEqual(cohort.count, 3)
+
+ response = self.client.get(f"/api/cohort/{cohort.pk}/persons")
+ self.assertEqual(len(response.json()["results"]), 3, response)
+
+ # if the batch is big enough, it's fewer queries
+ with self.assertNumQueries(9):
+ get_cohort_actors_for_feature_flag(cohort.pk, "some-feature2", self.team.pk, batchsize=10)
+
+ cohort.refresh_from_db()
+ self.assertEqual(cohort.name, "some cohort")
+ self.assertEqual(cohort.count, 3)
+
+ response = self.client.get(f"/api/cohort/{cohort.pk}/persons")
+ self.assertEqual(len(response.json()["results"]), 3, response)
+
class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
@snapshot_clickhouse_queries
diff --git a/posthog/api/test/test_organization_feature_flag.py b/posthog/api/test/test_organization_feature_flag.py
index a32fc86114033..78e72269b20bb 100644
--- a/posthog/api/test/test_organization_feature_flag.py
+++ b/posthog/api/test/test_organization_feature_flag.py
@@ -53,7 +53,7 @@ def test_get_feature_flag_success(self):
"email": self.user.email,
"is_email_verified": self.user.is_email_verified,
},
- "filters": flag.filters,
+ "filters": flag.get_filters(),
"created_at": flag.created_at.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z",
"active": flag.active,
}
@@ -243,6 +243,29 @@ def test_copy_feature_flag_update_existing(self):
set(flag_response.keys()),
)
+ def test_copy_feature_flag_with_old_legacy_flags(self):
+ url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags"
+ target_project = self.team_2
+
+ flag_to_copy = FeatureFlag.objects.create(
+ team=self.team_1,
+ created_by=self.user,
+ key="flag-to-copy-here",
+ filters={},
+ rollout_percentage=self.rollout_percentage_to_copy,
+ )
+
+ data = {
+ "feature_flag_key": flag_to_copy.key,
+ "from_project": self.feature_flag_to_copy.team_id,
+ "target_project_ids": [target_project.id],
+ }
+ response = self.client.post(url, data)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.json()["success"]), 1)
+ self.assertEqual(len(response.json()["failed"]), 0)
+
def test_copy_feature_flag_update_override_deleted(self):
target_project = self.team_2
target_project_2 = Team.objects.create(organization=self.organization)
diff --git a/posthog/api/test/test_search.py b/posthog/api/test/test_search.py
index 543d2d5adc048..36f31c8ec9ef4 100644
--- a/posthog/api/test/test_search.py
+++ b/posthog/api/test/test_search.py
@@ -74,11 +74,7 @@ def test_response_format_and_ids(self):
"rank": response.json()["results"][1]["rank"],
"type": "insight",
"result_id": self.insight_1.short_id,
- "extra_fields": {
- "derived_name": None,
- "name": "second insight",
- "description": None,
- },
+ "extra_fields": {"name": "second insight", "description": None, "filters": {}, "query": None},
},
)
@@ -88,7 +84,7 @@ def test_extra_fields(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json()["results"][0]["extra_fields"],
- {"derived_name": "derived name", "description": None, "name": None},
+ {"name": None, "description": None, "filters": {}, "query": None},
)
def test_search_with_fully_invalid_query(self):
diff --git a/posthog/api/test/test_signup.py b/posthog/api/test/test_signup.py
index e106dd6cbddf2..00c101e4487ee 100644
--- a/posthog/api/test/test_signup.py
+++ b/posthog/api/test/test_signup.py
@@ -3,9 +3,9 @@
from typing import Dict, Optional, cast
from unittest import mock
from unittest.mock import ANY, patch
+from zoneinfo import ZoneInfo
import pytest
-from zoneinfo import ZoneInfo
from django.core import mail
from django.urls.base import reverse
from django.utils import timezone
@@ -543,6 +543,7 @@ def test_social_signup_with_allowed_domain_on_self_hosted(
@patch("posthoganalytics.capture")
@mock.patch("ee.billing.billing_manager.BillingManager.update_billing_distinct_ids")
+ @mock.patch("ee.billing.billing_manager.BillingManager.update_billing_customer_email")
@mock.patch("social_core.backends.base.BaseAuth.request")
@mock.patch("posthog.api.authentication.get_instance_available_sso_providers")
@mock.patch("posthog.tasks.user_identify.identify_task")
@@ -553,11 +554,13 @@ def test_social_signup_with_allowed_domain_on_cloud(
mock_sso_providers,
mock_request,
mock_update_distinct_ids,
+ mock_update_billing_customer_email,
mock_capture,
):
with self.is_cloud(True):
self.run_test_for_allowed_domain(mock_sso_providers, mock_request, mock_capture)
assert mock_update_distinct_ids.called_once()
+ assert mock_update_billing_customer_email.called_once()
@mock.patch("social_core.backends.base.BaseAuth.request")
@mock.patch("posthog.api.authentication.get_instance_available_sso_providers")
diff --git a/posthog/api/test/test_team.py b/posthog/api/test/test_team.py
index 8924667fbbea8..0431abea04817 100644
--- a/posthog/api/test/test_team.py
+++ b/posthog/api/test/test_team.py
@@ -1,6 +1,7 @@
import json
from typing import List, cast
-from unittest.mock import ANY, MagicMock, patch
+from unittest import mock
+from unittest.mock import MagicMock, call, patch
from asgiref.sync import sync_to_async
from django.core.cache import cache
@@ -219,15 +220,16 @@ def test_delete_team_own_second(self, mock_capture: MagicMock, mock_delete_bulky
AsyncDeletion.objects.filter(team_id=team.id, deletion_type=DeletionType.Team, key=str(team.id)).count(),
1,
)
- mock_capture.assert_called_once_with(
- self.user.distinct_id,
- "team deleted",
- properties={},
- groups={
- "instance": ANY,
- "organization": str(self.organization.id),
- "project": str(self.team.uuid),
- },
+ mock_capture.assert_has_calls(
+ calls=[
+ call(
+ self.user.distinct_id,
+ "membership level changed",
+ properties={"new_level": 8, "previous_level": 1},
+ groups=mock.ANY,
+ ),
+ call(self.user.distinct_id, "team deleted", properties={}, groups=mock.ANY),
+ ]
)
mock_delete_bulky_postgres_data.assert_called_once_with(team_ids=[team.pk])
diff --git a/posthog/batch_exports/http.py b/posthog/batch_exports/http.py
index 8d6005ec663f8..cef17ab628f32 100644
--- a/posthog/batch_exports/http.py
+++ b/posthog/batch_exports/http.py
@@ -2,6 +2,7 @@
from typing import Any
import posthoganalytics
+import structlog
from django.db import transaction
from django.utils.timezone import now
from rest_framework import mixins, request, response, serializers, viewsets
@@ -27,6 +28,7 @@
BatchExportIdError,
BatchExportServiceError,
BatchExportServiceRPCError,
+ BatchExportServiceScheduleNotFound,
backfill_export,
cancel_running_batch_export_backfill,
delete_schedule,
@@ -49,6 +51,8 @@
from posthog.temporal.client import sync_connect
from posthog.utils import relative_date_parse
+logger = structlog.get_logger(__name__)
+
def validate_date_input(date_input: Any) -> dt.datetime:
"""Parse any datetime input as a proper dt.datetime.
@@ -320,10 +324,22 @@ def unpause(self, request: request.Request, *args, **kwargs) -> response.Respons
return response.Response({"paused": False})
def perform_destroy(self, instance: BatchExport):
- """Perform a BatchExport destroy by clearing Temporal and Django state."""
- instance.deleted = True
+ """Perform a BatchExport destroy by clearing Temporal and Django state.
+
+ If the underlying Temporal Schedule doesn't exist, we ignore the error and proceed with the delete anyways.
+ The Schedule could have been manually deleted causing Django and Temporal to go out of sync. For whatever reason,
+ since we are deleting, we assume that we can recover from this state by finishing the delete operation by calling
+ instance.save().
+ """
temporal = sync_connect()
- delete_schedule(temporal, str(instance.pk))
+
+ instance.deleted = True
+
+ try:
+ delete_schedule(temporal, str(instance.pk))
+ except BatchExportServiceScheduleNotFound as e:
+ logger.warning("The Schedule %s could not be deleted as it was not found", e.schedule_id)
+
instance.save()
for backfill in BatchExportBackfill.objects.filter(batch_export=instance):
diff --git a/posthog/batch_exports/service.py b/posthog/batch_exports/service.py
index fc74d6f51f253..38cecda263aaa 100644
--- a/posthog/batch_exports/service.py
+++ b/posthog/batch_exports/service.py
@@ -3,6 +3,7 @@
from dataclasses import asdict, dataclass, fields
from uuid import UUID
+import temporalio
from asgiref.sync import async_to_sync
from temporalio.client import (
Client,
@@ -163,6 +164,14 @@ class BatchExportServiceRPCError(BatchExportServiceError):
"""Exception raised when the underlying Temporal RPC fails."""
+class BatchExportServiceScheduleNotFound(BatchExportServiceRPCError):
+ """Exception raised when the underlying Temporal RPC fails because a schedule was not found."""
+
+ def __init__(self, schedule_id: str):
+ self.schedule_id = schedule_id
+ super().__init__(f"The Temporal Schedule {schedule_id} was not found (maybe it was deleted?)")
+
+
def pause_batch_export(temporal: Client, batch_export_id: str, note: str | None = None) -> None:
"""Pause this BatchExport.
@@ -250,7 +259,14 @@ async def unpause_schedule(temporal: Client, schedule_id: str, note: str | None
async def delete_schedule(temporal: Client, schedule_id: str) -> None:
"""Delete a Temporal Schedule."""
handle = temporal.get_schedule_handle(schedule_id)
- await handle.delete()
+
+ try:
+ await handle.delete()
+ except temporalio.service.RPCError as e:
+ if e.status == temporalio.service.RPCStatusCode.NOT_FOUND:
+ raise BatchExportServiceScheduleNotFound(schedule_id)
+ else:
+ raise BatchExportServiceRPCError() from e
@async_to_sync
diff --git a/posthog/event_usage.py b/posthog/event_usage.py
index fa69f0c23662b..7cd1945d37df4 100644
--- a/posthog/event_usage.py
+++ b/posthog/event_usage.py
@@ -196,6 +196,26 @@ def report_bulk_invited(
)
+def report_user_organization_membership_level_changed(
+ user: User,
+ organization: Organization,
+ new_level: int,
+ previous_level: int,
+) -> None:
+ """
+ Triggered after a user's membership level in an organization is changed.
+ """
+ posthoganalytics.capture(
+ user.distinct_id,
+ "membership level changed",
+ properties={
+ "new_level": new_level,
+ "previous_level": previous_level,
+ },
+ groups=groups(organization),
+ )
+
+
def report_user_action(user: User, event: str, properties: Dict = {}):
posthoganalytics.capture(
user.distinct_id,
diff --git a/posthog/hogql/test/__snapshots__/test_resolver.ambr b/posthog/hogql/test/__snapshots__/test_resolver.ambr
new file mode 100644
index 0000000000000..78223c03c2b66
--- /dev/null
+++ b/posthog/hogql/test/__snapshots__/test_resolver.ambr
@@ -0,0 +1,2984 @@
+# name: TestResolver.test_asterisk_expander_from_subquery_table
+ '
+ {
+ select: [
+ {
+ chain: [
+ "uuid"
+ ]
+ type: {
+ name: "uuid"
+ table_type: {
+ aliases: {}
+ anonymous_tables: []
+ columns: {
+ $group_0: {
+ name: "$group_0"
+ table_type: {
+ table: {
+ fields: {
+ $group_0: {},
+ $group_1: {},
+ $group_2: {},
+ $group_3: {},
+ $group_4: {},
+ $session_id: {},
+ created_at: {},
+ distinct_id: {},
+ elements_chain: {},
+ event: {},
+ goe_0: {},
+ goe_1: {},
+ goe_2: {},
+ goe_3: {},
+ goe_4: {},
+ group_0: {},
+ group_1: {},
+ group_2: {},
+ group_3: {},
+ group_4: {},
+ override: {},
+ override_person_id: {},
+ pdi: {},
+ person: {},
+ person_id: {},
+ poe: {},
+ properties: {},
+ session: {},
+ team_id: {},
+ timestamp: {},
+ uuid: {}
+ }
+ }
+ }
+ },
+ $group_1: {
+ name: "$group_1"
+ table_type:
+ },
+ $group_2: {
+ name: "$group_2"
+ table_type:
+ },
+ $group_3: {
+ name: "$group_3"
+ table_type:
+ },
+ $group_4: {
+ name: "$group_4"
+ table_type:
+ },
+ $session_id: {
+ name: "$session_id"
+ table_type:
+ },
+ created_at: {
+ name: "created_at"
+ table_type:
+ },
+ distinct_id: {
+ name: "distinct_id"
+ table_type:
+ },
+ elements_chain: {
+ name: "elements_chain"
+ table_type:
+ },
+ event: {
+ name: "event"
+ table_type:
+ },
+ properties: {
+ name: "properties"
+ table_type:
+ },
+ timestamp: {
+ name: "timestamp"
+ table_type:
+ },
+ uuid: {
+ name: "uuid"
+ table_type:
+ }
+ }
+ ctes: {}
+ tables: {
+ events:
+ }
+ }
+ }
+ },
+ {
+ chain: [
+ "event"
+ ]
+ type: {
+ name: "event"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "properties"
+ ]
+ type: {
+ name: "properties"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "timestamp"
+ ]
+ type: {
+ name: "timestamp"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "distinct_id"
+ ]
+ type: {
+ name: "distinct_id"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "elements_chain"
+ ]
+ type: {
+ name: "elements_chain"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "created_at"
+ ]
+ type: {
+ name: "created_at"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$session_id"
+ ]
+ type: {
+ name: "$session_id"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_0"
+ ]
+ type: {
+ name: "$group_0"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_1"
+ ]
+ type: {
+ name: "$group_1"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_2"
+ ]
+ type: {
+ name: "$group_2"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_3"
+ ]
+ type: {
+ name: "$group_3"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_4"
+ ]
+ type: {
+ name: "$group_4"
+ table_type:
+ }
+ }
+ ]
+ select_from: {
+ table: {
+ select: [
+ {
+ chain: [
+ "uuid"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "event"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "properties"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "timestamp"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "distinct_id"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "elements_chain"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "created_at"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$session_id"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_0"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_1"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_2"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_3"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_4"
+ ]
+ type:
+ }
+ ]
+ select_from: {
+ table: {
+ chain: [
+ "events"
+ ]
+ type:
+ }
+ type:
+ }
+ type:
+ }
+ type:
+ }
+ type: {
+ aliases: {}
+ anonymous_tables: [
+
+ ]
+ columns: {
+ $group_0: ,
+ $group_1: ,
+ $group_2: ,
+ $group_3: ,
+ $group_4: ,
+ $session_id: ,
+ created_at: ,
+ distinct_id: ,
+ elements_chain: ,
+ event: ,
+ properties: ,
+ timestamp: ,
+ uuid:
+ }
+ ctes: {}
+ tables: {}
+ }
+ }
+ '
+---
+# name: TestResolver.test_asterisk_expander_select_union
+ '
+ {
+ select: [
+ {
+ chain: [
+ "uuid"
+ ]
+ type: {
+ name: "uuid"
+ table_type: {
+ types: [
+ {
+ aliases: {}
+ anonymous_tables: []
+ columns: {
+ $group_0: {
+ name: "$group_0"
+ table_type: {
+ table: {
+ fields: {
+ $group_0: {},
+ $group_1: {},
+ $group_2: {},
+ $group_3: {},
+ $group_4: {},
+ $session_id: {},
+ created_at: {},
+ distinct_id: {},
+ elements_chain: {},
+ event: {},
+ goe_0: {},
+ goe_1: {},
+ goe_2: {},
+ goe_3: {},
+ goe_4: {},
+ group_0: {},
+ group_1: {},
+ group_2: {},
+ group_3: {},
+ group_4: {},
+ override: {},
+ override_person_id: {},
+ pdi: {},
+ person: {},
+ person_id: {},
+ poe: {},
+ properties: {},
+ session: {},
+ team_id: {},
+ timestamp: {},
+ uuid: {}
+ }
+ }
+ }
+ },
+ $group_1: {
+ name: "$group_1"
+ table_type:
+ },
+ $group_2: {
+ name: "$group_2"
+ table_type:
+ },
+ $group_3: {
+ name: "$group_3"
+ table_type:
+ },
+ $group_4: {
+ name: "$group_4"
+ table_type:
+ },
+ $session_id: {
+ name: "$session_id"
+ table_type:
+ },
+ created_at: {
+ name: "created_at"
+ table_type:
+ },
+ distinct_id: {
+ name: "distinct_id"
+ table_type:
+ },
+ elements_chain: {
+ name: "elements_chain"
+ table_type:
+ },
+ event: {
+ name: "event"
+ table_type:
+ },
+ properties: {
+ name: "properties"
+ table_type:
+ },
+ timestamp: {
+ name: "timestamp"
+ table_type:
+ },
+ uuid: {
+ name: "uuid"
+ table_type:
+ }
+ }
+ ctes: {}
+ tables: {
+ events:
+ }
+ },
+ {
+ aliases: {}
+ anonymous_tables: []
+ columns: {
+ $group_0: {
+ name: "$group_0"
+ table_type: {
+ table: {
+ fields: {
+ $group_0: {},
+ $group_1: {},
+ $group_2: {},
+ $group_3: {},
+ $group_4: {},
+ $session_id: {},
+ created_at: {},
+ distinct_id: {},
+ elements_chain: {},
+ event: {},
+ goe_0: {},
+ goe_1: {},
+ goe_2: {},
+ goe_3: {},
+ goe_4: {},
+ group_0: {},
+ group_1: {},
+ group_2: {},
+ group_3: {},
+ group_4: {},
+ override: {},
+ override_person_id: {},
+ pdi: {},
+ person: {},
+ person_id: {},
+ poe: {},
+ properties: {},
+ session: {},
+ team_id: {},
+ timestamp: {},
+ uuid: {}
+ }
+ }
+ }
+ },
+ $group_1: {
+ name: "$group_1"
+ table_type:
+ },
+ $group_2: {
+ name: "$group_2"
+ table_type:
+ },
+ $group_3: {
+ name: "$group_3"
+ table_type:
+ },
+ $group_4: {
+ name: "$group_4"
+ table_type:
+ },
+ $session_id: {
+ name: "$session_id"
+ table_type:
+ },
+ created_at: {
+ name: "created_at"
+ table_type:
+ },
+ distinct_id: {
+ name: "distinct_id"
+ table_type:
+ },
+ elements_chain: {
+ name: "elements_chain"
+ table_type:
+ },
+ event: {
+ name: "event"
+ table_type:
+ },
+ properties: {
+ name: "properties"
+ table_type:
+ },
+ timestamp: {
+ name: "timestamp"
+ table_type:
+ },
+ uuid: {
+ name: "uuid"
+ table_type:
+ }
+ }
+ ctes: {}
+ tables: {
+ events:
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ chain: [
+ "event"
+ ]
+ type: {
+ name: "event"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "properties"
+ ]
+ type: {
+ name: "properties"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "timestamp"
+ ]
+ type: {
+ name: "timestamp"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "distinct_id"
+ ]
+ type: {
+ name: "distinct_id"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "elements_chain"
+ ]
+ type: {
+ name: "elements_chain"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "created_at"
+ ]
+ type: {
+ name: "created_at"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$session_id"
+ ]
+ type: {
+ name: "$session_id"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_0"
+ ]
+ type: {
+ name: "$group_0"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_1"
+ ]
+ type: {
+ name: "$group_1"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_2"
+ ]
+ type: {
+ name: "$group_2"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_3"
+ ]
+ type: {
+ name: "$group_3"
+ table_type:
+ }
+ },
+ {
+ chain: [
+ "$group_4"
+ ]
+ type: {
+ name: "$group_4"
+ table_type:
+ }
+ }
+ ]
+ select_from: {
+ table: {
+ select_queries: [
+ {
+ select: [
+ {
+ chain: [
+ "uuid"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "event"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "properties"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "timestamp"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "distinct_id"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "elements_chain"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "created_at"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$session_id"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_0"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_1"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_2"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_3"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_4"
+ ]
+ type:
+ }
+ ]
+ select_from: {
+ table: {
+ chain: [
+ "events"
+ ]
+ type:
+ }
+ type:
+ }
+ type:
+ },
+ {
+ select: [
+ {
+ chain: [
+ "uuid"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "event"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "properties"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "timestamp"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "distinct_id"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "elements_chain"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "created_at"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$session_id"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_0"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_1"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_2"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_3"
+ ]
+ type:
+ },
+ {
+ chain: [
+ "$group_4"
+ ]
+ type:
+ }
+ ]
+ select_from: {
+ table: {
+ chain: [
+ "events"
+ ]
+ type:
+ }
+ type:
+ }
+ type:
+ }
+ ]
+ type:
+ }
+ type:
+ }
+ type: {
+ aliases: {}
+ anonymous_tables: [
+
+ ]
+ columns: {
+ $group_0: ,
+ $group_1: ,
+ $group_2: ,
+ $group_3: ,
+ $group_4: ,
+ $session_id: ,
+ created_at: ,
+ distinct_id: ,
+ elements_chain: ,
+ event: ,
+ properties: ,
+ timestamp: ,
+ uuid:
+ }
+ ctes: {}
+ tables: {}
+ }
+ }
+ '
+---
+# name: TestResolver.test_asterisk_expander_subquery
+ '
+ {
+ select: [
+ {
+ chain: [
+ "a"
+ ]
+ type: {
+ name: "a"
+ table_type: {
+ aliases: {
+ a: {
+ alias: "a"
+ type: {
+ data_type: "int"
+ }
+ },
+ b: {
+ alias: "b"
+ type: {
+ data_type: "int"
+ }
+ }
+ }
+ anonymous_tables: []
+ columns: {
+ a: ,
+ b:
+ }
+ ctes: {}
+ tables: {}
+ }
+ }
+ },
+ {
+ chain: [
+ "b"
+ ]
+ type: {
+ name: "b"
+ table_type:
+ }
+ }
+ ]
+ select_from: {
+ table: {
+ select: [
+ {
+ alias: "a"
+ expr: {
+ type:
+ value: 1
+ }
+ type:
+ },
+ {
+ alias: "b"
+ expr: {
+ type:
+ value: 2
+ }
+ type:
+ }
+ ]
+ type:
+ }
+ type:
+ }
+ type: {
+ aliases: {}
+ anonymous_tables: [
+
+ ]
+ columns: {
+ a: ,
+ b:
+ }
+ ctes: {}
+ tables: {}
+ }
+ }
+ '
+---
+# name: TestResolver.test_asterisk_expander_subquery_alias
+ '
+ {
+ select: [
+ {
+ chain: [
+ "a"
+ ]
+ type: {
+ name: "a"
+ table_type: {
+ alias: "x"
+ select_query_type: {
+ aliases: {
+ a: {
+ alias: "a"
+ type: {
+ data_type: "int"
+ }
+ },
+ b: {
+ alias: "b"
+ type: {
+ data_type: "int"
+ }
+ }
+ }
+ anonymous_tables: []
+ columns: {
+ a: ,
+ b:
+ }
+ ctes: {}
+ tables: {}
+ }
+ }
+ }
+ },
+ {
+ chain: [
+ "b"
+ ]
+ type: {
+ name: "b"
+ table_type:
+ }
+ }
+ ]
+ select_from: {
+ alias: "x"
+ table: {
+ select: [
+ {
+ alias: "a"
+ expr: {
+ type: