diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png index 0fa44c98b2c5b..6da90a8a2e7e1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--light.png index 7178f9989b67f..a131b2e67755b 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--light.png index b215929f99da6..276e305964386 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--light.png index 7178f9989b67f..a131b2e67755b 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--light.png differ diff --git a/frontend/src/lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic.ts b/frontend/src/lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic.ts index 5792dbf59b716..36fba3da699b6 100644 --- a/frontend/src/lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic.ts +++ b/frontend/src/lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic.ts @@ -162,6 +162,9 @@ export const iframedToolbarBrowserLogic = kea([ currentFullUrl: [ (s) => [s.browserUrl, s.currentPath], (browserUrl, currentPath) => { + if (!browserUrl) { + return null + } return browserUrl + '/' + currentPath }, ], @@ -325,6 +328,12 @@ export const iframedToolbarBrowserLogic = kea([ clearTimeout(cache.errorTimeout) clearTimeout(cache.warnTimeout) }, + setIframeBanner: ({ banner }) => { + posthog.capture('in-app iFrame banner set', { + level: banner?.level, + message: banner?.message, + }) + }, })), afterMount(({ actions, values }) => { diff --git a/frontend/src/scenes/dashboard/DashboardTemplateChooser.tsx b/frontend/src/scenes/dashboard/DashboardTemplateChooser.tsx index b631392b38c9d..a2a320120ff15 100644 --- a/frontend/src/scenes/dashboard/DashboardTemplateChooser.tsx +++ b/frontend/src/scenes/dashboard/DashboardTemplateChooser.tsx @@ -13,12 +13,13 @@ import { } from 'scenes/dashboard/dashboards/templates/dashboardTemplatesLogic' import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' -import { DashboardTemplateType } from '~/types' +import { DashboardTemplateType, TemplateAvailabilityContext } from '~/types' export function DashboardTemplateChooser({ scope = 'default', onItemClick, redirectAfterCreation = true, + availabilityContexts, }: DashboardTemplateProps): JSX.Element { const templatesLogic = dashboardTemplatesLogic({ scope }) const { allTemplates, allTemplatesLoading } = useValues(templatesLogic) @@ -35,61 +36,72 @@ export function DashboardTemplateChooser({ return (
- { - if (isLoading) { - return - } - setIsLoading(true) - addDashboard({ - name: 'New Dashboard', - show: true, - }) - }} - index={0} - data-attr="create-dashboard-blank" - /> + {!availabilityContexts || availabilityContexts.includes(TemplateAvailabilityContext.GENERAL) ? ( + { + if (isLoading) { + return + } + setIsLoading(true) + addDashboard({ + name: 'New Dashboard', + show: true, + }) + }} + index={0} + data-attr="create-dashboard-blank" + /> + ) : null} {allTemplatesLoading ? ( ) : ( - allTemplates.map((template, index) => ( - { - if (isLoading) { - return - } - setIsLoading(true) - // while we might receive templates from the external repository - // we need to handle templates that don't have variables - if ((template.variables || []).length === 0) { - if (template.variables === null) { - template.variables = [] + allTemplates + .filter((template) => { + if (availabilityContexts) { + return availabilityContexts.some((context) => + template.availability_contexts?.includes(context) + ) + } + return true + }) + .map((template, index) => ( + { + if (isLoading) { + return } - createDashboardFromTemplate( - template, - template.variables || [], - redirectAfterCreation - ) - } else { - if (!newDashboardModalVisible) { - showVariableSelectModal(template) + setIsLoading(true) + // while we might receive templates from the external repository + // we need to handle templates that don't have variables + if ((template.variables || []).length === 0) { + if (template.variables === null) { + template.variables = [] + } + createDashboardFromTemplate( + template, + template.variables || [], + redirectAfterCreation + ) } else { - setActiveDashboardTemplate(template) + if (!newDashboardModalVisible) { + showVariableSelectModal(template) + } else { + setActiveDashboardTemplate(template) + } } - } - onItemClick?.(template) - }} - index={index + 1} - data-attr="create-dashboard-from-template" - /> - )) + onItemClick?.(template) + }} + index={index + 1} + data-attr="create-dashboard-from-template" + /> + )) )}
diff --git a/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts b/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts index de17c8108209a..506147c014565 100644 --- a/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts +++ b/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts @@ -40,6 +40,7 @@ export const dashboardTemplateVariablesLogic = kea ({ variableName, action }), setVariableForPageview: (variableName: string, url: string) => ({ variableName, url }), + setVariableForScreenview: (variableName: string) => ({ variableName }), setActiveVariableIndex: (index: number) => ({ index }), incrementActiveVariableIndex: true, possiblyIncrementActiveVariableIndex: true, @@ -47,6 +48,7 @@ export const dashboardTemplateVariablesLogic = kea ({ isSelecting }), setActiveVariableCustomEventName: (customEventName?: string | null) => ({ customEventName }), + maybeResetActiveVariableCustomEventName: true, }), reducers({ variables: [ @@ -194,12 +196,34 @@ export const dashboardTemplateVariablesLogic = kea { + const step: TemplateVariableStep = { + id: '$screenview', + math: BaseMathType.UniqueUsers, + type: EntityTypes.EVENTS, + order: 0, + name: '$screenview', + custom_name: variableName, + } + const filterGroup: FilterType = { + events: [step], + } + actions.setVariable(variableName, filterGroup) + actions.setIsCurrentlySelectingElement(false) + }, toolbarMessageReceived: ({ type, payload }) => { if (type === PostHogAppToolbarEvent.PH_NEW_ACTION_CREATED) { actions.setVariableFromAction(payload.action.name, payload.action as ActionType) actions.disableElementSelector() } }, + maybeResetActiveVariableCustomEventName: () => { + if (!values.activeVariable?.touched || !values.activeVariable?.default?.custom_event) { + actions.setActiveVariableCustomEventName(null) + } else if (values.activeVariable?.default?.custom_event) { + actions.setActiveVariableCustomEventName(values.activeVariable.default.id) + } + }, })), propsChanged(({ actions, props }, oldProps) => { if (props.variables !== oldProps.variables) { diff --git a/frontend/src/scenes/dashboard/dashboards/templates/dashboardTemplatesLogic.tsx b/frontend/src/scenes/dashboard/dashboards/templates/dashboardTemplatesLogic.tsx index c3c8a58c10817..3da908e2c3f27 100644 --- a/frontend/src/scenes/dashboard/dashboards/templates/dashboardTemplatesLogic.tsx +++ b/frontend/src/scenes/dashboard/dashboards/templates/dashboardTemplatesLogic.tsx @@ -3,7 +3,7 @@ import { loaders } from 'kea-loaders' import api from 'lib/api' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { DashboardTemplateScope, DashboardTemplateType } from '~/types' +import { DashboardTemplateScope, DashboardTemplateType, TemplateAvailabilityContext } from '~/types' import type { dashboardTemplatesLogicType } from './dashboardTemplatesLogicType' @@ -12,6 +12,7 @@ export interface DashboardTemplateProps { scope?: 'default' | DashboardTemplateScope onItemClick?: (template: DashboardTemplateType) => void redirectAfterCreation?: boolean + availabilityContexts?: TemplateAvailabilityContext[] } export const dashboardTemplatesLogic = kea([ diff --git a/frontend/src/scenes/dashboard/newDashboardLogic.ts b/frontend/src/scenes/dashboard/newDashboardLogic.ts index fb83b3ca15b70..44fa0252f5889 100644 --- a/frontend/src/scenes/dashboard/newDashboardLogic.ts +++ b/frontend/src/scenes/dashboard/newDashboardLogic.ts @@ -79,11 +79,13 @@ export const newDashboardLogic = kea([ createDashboardFromTemplate: ( template: DashboardTemplateType, variables: DashboardTemplateVariableType[], - redirectAfterCreation?: boolean + redirectAfterCreation?: boolean, + creationContext: string | null = null ) => ({ template, variables, redirectAfterCreation, + creationContext, }), submitNewDashboardSuccessWithResult: (result: DashboardType, variables?: DashboardTemplateVariableType[]) => ({ result, @@ -178,7 +180,12 @@ export const newDashboardLogic = kea([ actions.clearActiveDashboardTemplate() actions.resetNewDashboard() }, - createDashboardFromTemplate: async ({ template, variables, redirectAfterCreation = true }) => { + createDashboardFromTemplate: async ({ + template, + variables, + redirectAfterCreation = true, + creationContext = null, + }) => { actions.setIsLoading(true) const tiles = makeTilesUsingVariables(template.tiles, variables) const dashboardJSON = { @@ -189,7 +196,7 @@ export const newDashboardLogic = kea([ try { const result: DashboardType = await api.create( `api/projects/${teamLogic.values.currentTeamId}/dashboards/create_from_template_json`, - { template: dashboardJSON } + { template: dashboardJSON, creation_context: creationContext } ) actions.hideNewDashboardModal() actions.resetNewDashboard() diff --git a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx index 6b41d63cc00fe..c4f636e174bba 100644 --- a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx +++ b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx @@ -28,19 +28,12 @@ const UrlInput = ({ iframeRef }: { iframeRef: React.RefObject const { setBrowserUrl, setInitialPath } = useActions( iframedToolbarBrowserLogic({ iframeRef, clearBrowserUrlOnUnmount: true }) ) - const { browserUrl, currentPath, currentFullUrl } = useValues( + const { browserUrl, currentPath } = useValues( iframedToolbarBrowserLogic({ iframeRef, clearBrowserUrlOnUnmount: true }) ) const { snippetHosts } = useValues(sdksLogic) const { addUrl } = useActions(authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS })) const [inputValue, setInputValue] = useState(currentPath) - const { activeDashboardTemplate } = useValues(newDashboardLogic) - const theDashboardTemplateVariablesLogic = dashboardTemplateVariablesLogic({ - variables: activeDashboardTemplate?.variables || [], - }) - const { setVariableForPageview, setActiveVariableCustomEventName } = useActions(theDashboardTemplateVariablesLogic) - const { activeVariable } = useValues(theDashboardTemplateVariablesLogic) - const { hideCustomEventField } = useActions(onboardingTemplateConfigLogic) useEffect(() => { setInputValue(currentPath) @@ -87,18 +80,6 @@ const UrlInput = ({ iframeRef }: { iframeRef: React.RefObject setInitialPath(inputValue || '') }} /> - { - setVariableForPageview(activeVariable.name, currentFullUrl) - setActiveVariableCustomEventName(null) - hideCustomEventField() - }} - > - Select pageview - ) } @@ -272,6 +253,7 @@ export const OnboardingDashboardTemplateConfigureStep = ({ {' '} (no need to send it now) .

+

PS! These don't have to be perfect, you can fine-tune them later.

diff --git a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateSelectStep.tsx b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateSelectStep.tsx index ce488672dd8d1..0c437a6594e83 100644 --- a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateSelectStep.tsx +++ b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateSelectStep.tsx @@ -4,6 +4,8 @@ import { useEffect } from 'react' import { DashboardTemplateChooser } from 'scenes/dashboard/DashboardTemplateChooser' import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { TemplateAvailabilityContext } from '~/types' + import { onboardingLogic, OnboardingStepKey } from '../onboardingLogic' import { OnboardingStep } from '../OnboardingStep' import { onboardingTemplateConfigLogic } from './onboardingTemplateConfigLogic' @@ -15,7 +17,7 @@ export const OnboardingDashboardTemplateSelectStep = ({ }): JSX.Element => { const { goToNextStep } = useActions(onboardingLogic) const { clearActiveDashboardTemplate } = useActions(newDashboardLogic) - const { setDashboardCreatedDuringOnboarding } = useActions(onboardingTemplateConfigLogic) + const { setDashboardCreatedDuringOnboarding, reportTemplateSelected } = useActions(onboardingTemplateConfigLogic) // TODO: this is hacky, find a better way to clear the active template when coming back to this screen useEffect(() => { @@ -44,12 +46,15 @@ export const OnboardingDashboardTemplateSelectStep = ({

{ + // clear the saved dashboard so we don't skip the next step setDashboardCreatedDuringOnboarding(null) + reportTemplateSelected(template) if (template.variables?.length && template.variables.length > 0) { goToNextStep() } }} redirectAfterCreation={false} + availabilityContexts={[TemplateAvailabilityContext.ONBOARDING]} /> ) diff --git a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateVariables.tsx b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateVariables.tsx index 074002255b63b..48736120c2ba6 100644 --- a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateVariables.tsx +++ b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateVariables.tsx @@ -1,5 +1,5 @@ import { IconCheckCircle, IconInfo, IconTarget, IconTrash } from '@posthog/icons' -import { LemonBanner, LemonButton, LemonCollapse, LemonInput, LemonLabel, Spinner } from '@posthog/lemon-ui' +import { LemonBanner, LemonButton, LemonCollapse, LemonInput, LemonLabel, LemonMenu, Spinner } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { iframedToolbarBrowserLogic } from 'lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic' import { useEffect } from 'react' @@ -25,6 +25,8 @@ function VariableSelector({ }) const { setVariable, + setVariableForPageview, + setVariableForScreenview, resetVariable, goToNextUntouchedActiveVariableIndex, incrementActiveVariableIndex, @@ -43,6 +45,9 @@ function VariableSelector({ const { enableElementSelector, disableElementSelector, setNewActionName } = useActions( iframedToolbarBrowserLogic({ iframeRef, clearBrowserUrlOnUnmount: true }) ) + const { currentFullUrl, browserUrl, currentPath } = useValues( + iframedToolbarBrowserLogic({ iframeRef, clearBrowserUrlOnUnmount: true }) + ) const variable: DashboardTemplateVariableType | undefined = variables.find((v) => v.name === variableName) if (!variable) { @@ -85,9 +90,15 @@ function VariableSelector({ Page URL: {variable.default.url || 'any url'}

+ ) : variable.default.type === EntityTypes.EVENTS && + variable.default.name == '$screenview' ? ( +

+ Screenview:{' '} + {variable.default.properties?.[0].value || 'any screenview'} +

) : variable.default.type === EntityTypes.EVENTS ? (

- Pageview URL:{' '} + Pageview URL contains:{' '} {variable.default.properties?.[0].value || 'any url'}

) : null} @@ -113,11 +124,12 @@ function VariableSelector({
{ if (v) { setActiveVariableCustomEventName(v) setVariable(variable.name, { - events: [{ id: v, math: 'dau', type: 'events' }], + events: [{ id: v, math: 'dau', type: 'events', custom_event: true }], }) } else { setActiveVariableCustomEventName(null) @@ -127,7 +139,14 @@ function VariableSelector({ onBlur={() => { if (activeVariableCustomEventName) { setVariable(variable.name, { - events: [{ id: activeVariableCustomEventName, math: 'dau', type: 'events' }], + events: [ + { + id: activeVariableCustomEventName, + math: 'dau', + type: 'events', + custom_event: true, + }, + ], }) } else { resetVariable(variable.id) @@ -161,13 +180,14 @@ function VariableSelector({ - !allVariablesAreTouched + onClick={() => { + customEventFieldShown && hideCustomEventField() + allVariablesAreTouched ? goToNextUntouchedActiveVariableIndex() : variables.length !== activeVariableIndex + 1 ? incrementActiveVariableIndex() : null - } + }} > Continue @@ -207,6 +227,41 @@ function VariableSelector({ Select from site )} + + This pageview{' '} + {currentFullUrl ? ( +
+ {!currentPath ? browserUrl : '/' + currentPath} +
+ ) : null} +
+ ), + onClick: () => setVariableForPageview(variable.name, currentFullUrl || ''), + disabledReason: !currentFullUrl + ? 'Please select a site to use a specific pageview' + : undefined, + }, + { + label: 'Any pageview', + onClick: () => setVariableForPageview(variable.name, browserUrl || ''), + }, + { + label: 'Any screenview (mobile apps)', + onClick: () => setVariableForScreenview(variable.name), + }, + ], + }, + ]} + > + Use pageview + { diff --git a/frontend/src/scenes/onboarding/productAnalyticsSteps/onboardingTemplateConfigLogic.ts b/frontend/src/scenes/onboarding/productAnalyticsSteps/onboardingTemplateConfigLogic.ts index 51132bf69b9ca..6dffe47f5f3bc 100644 --- a/frontend/src/scenes/onboarding/productAnalyticsSteps/onboardingTemplateConfigLogic.ts +++ b/frontend/src/scenes/onboarding/productAnalyticsSteps/onboardingTemplateConfigLogic.ts @@ -1,9 +1,11 @@ import { actions, connect, kea, listeners, path, reducers } from 'kea' import { urlToAction } from 'kea-router' +import posthog from 'posthog-js' +import { dashboardTemplateVariablesLogic } from 'scenes/dashboard/dashboardTemplateVariablesLogic' import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' import { urls } from 'scenes/urls' -import { DashboardType } from '~/types' +import { DashboardTemplateType, DashboardType } from '~/types' import { onboardingLogic, OnboardingStepKey } from '../onboardingLogic' import type { onboardingTemplateConfigLogicType } from './onboardingTemplateConfigLogicType' @@ -14,10 +16,17 @@ import type { onboardingTemplateConfigLogicType } from './onboardingTemplateConf export const onboardingTemplateConfigLogic = kea([ path(['scenes', 'onboarding', 'productAnalyticsSteps', 'onboardingTemplateConfigLogic']), connect({ - values: [newDashboardLogic, ['activeDashboardTemplate']], + values: [newDashboardLogic, ['activeDashboardTemplate'], dashboardTemplateVariablesLogic, ['activeVariable']], actions: [ newDashboardLogic, ['submitNewDashboardSuccessWithResult', 'setIsLoading'], + dashboardTemplateVariablesLogic, + [ + 'setActiveVariableIndex', + 'incrementActiveVariableIndex', + 'setActiveVariableCustomEventName', + 'maybeResetActiveVariableCustomEventName', + ], onboardingLogic, ['goToPreviousStep', 'setOnCompleteOnboardingRedirectUrl'], ], @@ -26,6 +35,7 @@ export const onboardingTemplateConfigLogic = kea ({ dashboard }), showCustomEventField: true, hideCustomEventField: true, + reportTemplateSelected: (template: DashboardTemplateType) => ({ template }), }), reducers({ dashboardCreatedDuringOnboarding: [ @@ -44,13 +54,42 @@ export const onboardingTemplateConfigLogic = kea ({ + listeners(({ actions, values }) => ({ submitNewDashboardSuccessWithResult: ({ result, variables }) => { if (result && variables?.length == 0) { // dashbboard was created without variables, go to next step for success message onboardingLogic.actions.goToNextStep() } actions.setOnCompleteOnboardingRedirectUrl(urls.dashboard(result.id)) + posthog.capture('dashboard created during onboarding', { + dashboard_id: result.id, + creation_mode: result.creation_mode, + title: result.name, + has_variables: variables?.length ? variables?.length > 0 : false, + total_variables: variables?.length || 0, + variables: variables?.map((v) => v.name), + }) + }, + reportTemplateSelected: ({ template }) => { + posthog.capture('template selected during onboarding', { + template_id: template.id, + template_name: template.template_name, + variables: template.variables?.map((v) => v.name), + }) + }, + setActiveVariableIndex: () => { + actions.maybeResetActiveVariableCustomEventName() + }, + incrementActiveVariableIndex: () => { + actions.maybeResetActiveVariableCustomEventName() + }, + maybeResetActiveVariableCustomEventName: () => { + if (values.activeVariable.default?.custom_event) { + actions.showCustomEventField() + actions.setActiveVariableCustomEventName(values.activeVariable?.default?.id) + } else { + actions.hideCustomEventField() + } }, })), urlToAction(({ actions, values }) => ({ diff --git a/frontend/src/toolbar/actions/actionsTabLogic.tsx b/frontend/src/toolbar/actions/actionsTabLogic.tsx index be2b0fa13f160..e9bde322fca0e 100644 --- a/frontend/src/toolbar/actions/actionsTabLogic.tsx +++ b/frontend/src/toolbar/actions/actionsTabLogic.tsx @@ -197,6 +197,7 @@ export const actionsTabLogic = kea([ const actionToSave = { ...formValues, steps: formValues.steps?.map(stepToDatabaseFormat) || [], + creation_context: values.automaticActionCreationEnabled ? 'onboarding' : null, } const { apiURL, temporaryToken } = values const { selectedActionId } = values diff --git a/frontend/src/types.ts b/frontend/src/types.ts index b8bb542b71625..173061faddd10 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1795,6 +1795,11 @@ export interface DashboardType extends DashboardBasicType { filters: DashboardFilter } +export enum TemplateAvailabilityContext { + GENERAL = 'general', + ONBOARDING = 'onboarding', +} + export interface DashboardTemplateType { id: string team_id?: number @@ -1807,6 +1812,7 @@ export interface DashboardTemplateType { tags?: string[] image_url?: string scope?: DashboardTemplateScope + availability_contexts?: TemplateAvailabilityContext[] } export interface MonacoMarker { @@ -2172,6 +2178,7 @@ export interface TemplateVariableStep { url?: string | null properties?: Record[] custom_name?: string + custom_event?: boolean } export interface PropertiesTimelineFilterType { diff --git a/latest_migrations.manifest b/latest_migrations.manifest index d0fb78b374262..ae3ec24fd92dc 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name ee: 0016_rolemembership_organization_member otp_static: 0002_throttling otp_totp: 0002_auto_20190420_0723 -posthog: 0472_experiment_metrics +posthog: 0473_dashboardtemplate_availability_contexts sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/posthog/api/action.py b/posthog/api/action.py index 4a669cef0d398..bd05b681e7c00 100644 --- a/posthog/api/action.py +++ b/posthog/api/action.py @@ -1,17 +1,15 @@ +from datetime import UTC, datetime from typing import Any, cast -from rest_framework import serializers, viewsets from django.db.models import Count -from rest_framework import request +from rest_framework import request, serializers, viewsets from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework_csv import renderers as csvrenderers from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer -from posthog.auth import ( - TemporaryTokenAuthentication, -) +from posthog.auth import TemporaryTokenAuthentication from posthog.constants import TREND_FILTER_TYPE_EVENTS from posthog.event_usage import report_user_action from posthog.models import Action @@ -19,7 +17,6 @@ from .forbid_destroy_model import ForbidDestroyModel from .tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin -from datetime import datetime, UTC class ActionStepJSONSerializer(serializers.Serializer): @@ -40,6 +37,7 @@ class ActionSerializer(TaggedItemSerializerMixin, serializers.HyperlinkedModelSe created_by = UserBasicSerializer(read_only=True) is_calculating = serializers.SerializerMethodField() is_action = serializers.BooleanField(read_only=True, default=True) + creation_context = serializers.SerializerMethodField() class Meta: model = Action @@ -60,6 +58,7 @@ class Meta: "is_action", "bytecode_error", "pinned_at", + "creation_context", ] read_only_fields = [ "team_id", @@ -70,6 +69,9 @@ class Meta: def get_is_calculating(self, action: Action) -> bool: return False + def get_creation_context(self, obj): + return None + def validate(self, attrs): instance = cast(Action, self.instance) exclude_args = {} @@ -96,13 +98,14 @@ def validate(self, attrs): return attrs def create(self, validated_data: Any) -> Any: + creation_context = self.context["request"].data.get("creation_context") validated_data["created_by"] = self.context["request"].user instance = super().create(validated_data) report_user_action( validated_data["created_by"], "action created", - instance.get_analytics_metadata(), + {**instance.get_analytics_metadata(), "creation_context": creation_context}, ) return instance diff --git a/posthog/api/dashboards/dashboard.py b/posthog/api/dashboards/dashboard.py index 7a0ab9b265cd1..5a15fe513a008 100644 --- a/posthog/api/dashboards/dashboard.py +++ b/posthog/api/dashboards/dashboard.py @@ -6,7 +6,6 @@ from django.shortcuts import get_object_or_404 from django.utils.timezone import now from rest_framework import exceptions, serializers, viewsets -from posthog.api.utils import action from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.request import Request from rest_framework.response import Response @@ -18,10 +17,11 @@ ) from posthog.api.forbid_destroy_model import ForbidDestroyModel from posthog.api.insight import InsightSerializer, InsightViewSet -from posthog.api.monitoring import monitor, Feature +from posthog.api.monitoring import Feature, monitor from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin +from posthog.api.utils import action from posthog.event_usage import report_user_action from posthog.helpers import create_dashboard_from_template from posthog.helpers.dashboard_templates import create_from_template @@ -517,6 +517,7 @@ def create_from_template_json(self, request: Request, *args: Any, **kwargs: Any) try: dashboard_template = DashboardTemplate(**request.data["template"]) + creation_context = request.data.get("creation_context") create_from_template(dashboard, dashboard_template) report_user_action( @@ -528,6 +529,7 @@ def create_from_template_json(self, request: Request, *args: Any, **kwargs: Any) "template_key": dashboard_template.template_name, "duplicated": False, "dashboard_id": dashboard.pk, + "creation_context": creation_context, }, ) except Exception: diff --git a/posthog/api/dashboards/dashboard_templates.py b/posthog/api/dashboards/dashboard_templates.py index 39941ff8b17fe..481c3f5363c34 100644 --- a/posthog/api/dashboards/dashboard_templates.py +++ b/posthog/api/dashboards/dashboard_templates.py @@ -6,13 +6,13 @@ from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from rest_framework import request, response, serializers, viewsets -from posthog.api.utils import action from rest_framework.exceptions import ValidationError from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.request import Request from posthog.api.forbid_destroy_model import ForbidDestroyModel from posthog.api.routing import TeamAndOrgViewSetMixin +from posthog.api.utils import action from posthog.models.dashboard_templates import DashboardTemplate logger = structlog.get_logger(__name__) @@ -47,6 +47,7 @@ class Meta: "image_url", "team_id", "scope", + "availability_contexts", ] def create(self, validated_data: dict, *args, **kwargs) -> DashboardTemplate: diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index dcef18134fecd..359389b8d65fa 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -60,6 +60,7 @@ '/home/runner/work/posthog/posthog/ee/api/role.py: Warning [RoleViewSet > RoleSerializer]: unable to resolve type hint for function "get_associated_flags". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/ee/api/role.py: Warning [RoleMembershipViewSet]: could not derive type of path parameter "organization_id" because model "ee.models.role.RoleMembership" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/action.py: Warning [ActionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.action.action.Action" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/action.py: Warning [ActionViewSet > ActionSerializer]: unable to resolve type hint for function "get_creation_context". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/api/activity_log.py: Warning [ActivityLogViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.activity_logging.activity_log.ActivityLog" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/annotation.py: Warning [AnnotationsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.annotation.Annotation" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/cohort.py: Warning [CohortViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.cohort.cohort.Cohort" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', diff --git a/posthog/api/test/dashboards/test_dashboard.py b/posthog/api/test/dashboards/test_dashboard.py index 359cff26345af..9b0625ec6fec4 100644 --- a/posthog/api/test/dashboards/test_dashboard.py +++ b/posthog/api/test/dashboards/test_dashboard.py @@ -1152,7 +1152,7 @@ def test_relations_on_insights_when_dashboards_were_deleted(self) -> None: def test_create_from_template_json(self, mock_capture) -> None: response = self.client.post( f"/api/projects/{self.team.id}/dashboards/create_from_template_json", - {"template": valid_template}, + {"template": valid_template, "creation_context": "onboarding"}, ) self.assertEqual(response.status_code, 200, response.content) @@ -1173,6 +1173,7 @@ def test_create_from_template_json(self, mock_capture) -> None: "dashboard created", { "created_at": mock.ANY, + "creation_context": "onboarding", "dashboard_id": dashboard["id"], "duplicated": False, "from_template": True, diff --git a/posthog/api/test/test_action.py b/posthog/api/test/test_action.py index f39a72db9b5cc..4228a653062f3 100644 --- a/posthog/api/test/test_action.py +++ b/posthog/api/test/test_action.py @@ -56,6 +56,7 @@ def test_create_action(self, patch_capture, *args): "created_by": ANY, "pinned_at": None, "deleted": False, + "creation_context": None, "is_calculating": False, "last_calculated_at": ANY, "team_id": self.team.id, @@ -82,6 +83,7 @@ def test_create_action(self, patch_capture, *args): "deleted": False, "pinned": False, "pinned_at": None, + "creation_context": None, }, ) diff --git a/posthog/api/test/test_survey.py b/posthog/api/test/test_survey.py index e5aa59dd4fd79..855e5171d3ce7 100644 --- a/posthog/api/test/test_survey.py +++ b/posthog/api/test/test_survey.py @@ -6,16 +6,14 @@ import pytest from django.core.cache import cache from django.test.client import Client - from freezegun.api import freeze_time -from posthog.api.survey import nh3_clean_with_allow_list -from posthog.models.cohort.cohort import Cohort from nanoid import generate from rest_framework import status +from posthog.api.survey import nh3_clean_with_allow_list from posthog.constants import AvailableFeature -from posthog.models import FeatureFlag, Action - +from posthog.models import Action, FeatureFlag +from posthog.models.cohort.cohort import Cohort from posthog.models.feedback.survey import Survey from posthog.test.base import ( APIBaseTest, @@ -2555,6 +2553,7 @@ def test_list_surveys_with_actions(self): "created_by": None, "deleted": False, "is_calculating": False, + "creation_context": None, "last_calculated_at": ANY, "team_id": self.team.id, "is_action": True, diff --git a/posthog/hogql/database/test/__snapshots__/test_database.ambr b/posthog/hogql/database/test/__snapshots__/test_database.ambr index 6a8f5f45be0b1..ebd12e015c813 100644 --- a/posthog/hogql/database/test/__snapshots__/test_database.ambr +++ b/posthog/hogql/database/test/__snapshots__/test_database.ambr @@ -219,7 +219,7 @@ "pdi" ], "hogql_value": "person", - "id": null, + "id": "person", "name": "person", "schema_valid": true, "table": "persons", @@ -472,7 +472,7 @@ "person" ], "hogql_value": "override", - "id": null, + "id": "override", "name": "override", "schema_valid": true, "table": "person_distinct_id_overrides", diff --git a/posthog/migrations/0473_dashboardtemplate_availability_contexts.py b/posthog/migrations/0473_dashboardtemplate_availability_contexts.py new file mode 100644 index 0000000000000..5d83c95e35e0c --- /dev/null +++ b/posthog/migrations/0473_dashboardtemplate_availability_contexts.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.15 on 2024-09-17 19:02 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0472_experiment_metrics"), + ] + + operations = [ + migrations.AddField( + model_name="dashboardtemplate", + name="availability_contexts", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=255), blank=True, null=True, size=None + ), + ), + ] diff --git a/posthog/models/dashboard_templates.py b/posthog/models/dashboard_templates.py index bf9d6dc733eb2..312b0013a2058 100644 --- a/posthog/models/dashboard_templates.py +++ b/posthog/models/dashboard_templates.py @@ -37,6 +37,8 @@ class Scope(models.TextChoices): # see https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers # but GitHub apparently is more likely 8kb https://stackoverflow.com/a/64565317 github_url = models.CharField(max_length=8201, null=True) + # where this template is available, e.g. "general" and/or "onboarding" + availability_contexts = ArrayField(models.CharField(max_length=255), blank=True, null=True) class Meta: constraints = [