Skip to content

Commit

Permalink
chore: wire cross sells into product intent logic (PostHog#27767)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
joshsny and github-actions[bot] authored Jan 27, 2025
1 parent c0d674f commit 63afbcc
Show file tree
Hide file tree
Showing 14 changed files with 108 additions and 48 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ import {
EVENT_PROPERTY_DEFINITIONS_PER_PAGE,
LOGS_PORTION_LIMIT,
} from './constants'
import type { ProductIntentProperties } from './utils/product-intents'

/**
* WARNING: Be very careful importing things here. This file is heavily used and can trigger a lot of cyclic imports
Expand Down Expand Up @@ -992,6 +993,10 @@ class ApiRequest {
public dataColorTheme(id: DataColorThemeModel['id'], teamId?: TeamType['id']): ApiRequest {
return this.environmentsDetail(teamId).addPathComponent('data_color_themes').addPathComponent(id)
}

public addProductIntent(): ApiRequest {
return this.environments().current().addPathComponent('add_product_intent')
}
}

const normalizeUrl = (url: string): string => {
Expand Down Expand Up @@ -2627,6 +2632,11 @@ const api = {
return await new ApiRequest().dataColorTheme(id).update({ data })
},
},
productIntents: {
async update(data: ProductIntentProperties): Promise<TeamType> {
return await new ApiRequest().addProductIntent().update({ data })
},
},

queryURL: (): string => {
return new ApiRequest().query().assembleFullUrl(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { LemonButton } from '@posthog/lemon-ui'
import { useActions } from 'kea'
import { router } from 'kea-router'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { ProductCrossSellLocation, trackProductCrossSell } from 'lib/utils/cross-sell'
import { ProductIntentContext } from 'lib/utils/product-intents'
import type React from 'react'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'

import { PipelineStage, ProductKey } from '~/types'
Expand All @@ -25,6 +26,7 @@ type EmptyStateProps = {

const EmptyState = ({ title, description, action, docsUrl, hog: Hog, groupType }: EmptyStateProps): JSX.Element => {
const { push } = useActions(router)
const { addProductIntentForCrossSell } = useActions(teamLogic)

return (
<div className="w-full p-8 rounded mt-4 flex items-center gap-4">
Expand All @@ -39,11 +41,10 @@ const EmptyState = ({ title, description, action, docsUrl, hog: Hog, groupType }
type="primary"
icon={<IconPlus />}
onClick={() => {
trackProductCrossSell({
addProductIntentForCrossSell({
from: ProductKey.PRODUCT_ANALYTICS,
to: ProductKey.DATA_WAREHOUSE,
location: ProductCrossSellLocation.TAXONOMIC_FILTER_EMPTY_STATE,
context: {},
intent_context: ProductIntentContext.TAXONOMIC_FILTER_EMPTY_STATE,
})

push(action.to)
Expand Down
21 changes: 0 additions & 21 deletions frontend/src/lib/utils/cross-sell.ts

This file was deleted.

54 changes: 54 additions & 0 deletions frontend/src/lib/utils/product-intents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import api from 'lib/api'

import type { ProductKey, TeamType } from '~/types'

export enum ProductIntentContext {
// Onboarding
ONBOARDING_PRODUCT_SELECTED_PRIMARY = 'onboarding product selected - primary',
ONBOARDING_PRODUCT_SELECTED_SECONDARY = 'onboarding product selected - secondary',

// Data Warehouse
SELECTED_CONNECTOR = 'selected connector',

// Experiments
EXPERIMENT_CREATED = 'experiment created',

// Feature Flags
FEATURE_FLAG_CREATED = 'feature flag created',

// Cross Sells
TAXONOMIC_FILTER_EMPTY_STATE = 'taxonomic filter empty state',
WEB_ANALYTICS_INSIGHT = 'web_analytics_insight',
}

export type ProductIntentMetadata = Record<string, unknown>

export type ProductIntentProperties = {
product_type: ProductKey
intent_context: ProductIntentContext
metadata?: ProductIntentMetadata
}

export function addProductIntent(properties: ProductIntentProperties): Promise<TeamType | null> {
return api.productIntents.update(properties)
}

export type ProductCrossSellProperties = {
from: ProductKey
to: ProductKey
intent_context: ProductIntentContext
metadata?: ProductIntentMetadata
}

export function addProductIntentForCrossSell(properties: ProductCrossSellProperties): Promise<TeamType | null> {
return api.productIntents.update({
product_type: properties.to,
intent_context: properties.intent_context,
metadata: {
...properties.metadata,
from: properties.from,
to: properties.to,
type: 'cross_sell',
},
})
}
6 changes: 5 additions & 1 deletion frontend/src/scenes/data-warehouse/new/sourceWizardLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { actions, connect, kea, listeners, path, props, reducers, selectors } fr
import { forms } from 'kea-forms'
import { router, urlToAction } from 'kea-router'
import api from 'lib/api'
import { ProductIntentContext } from 'lib/utils/product-intents'
import posthog from 'posthog-js'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
import { Scene } from 'scenes/sceneTypes'
Expand Down Expand Up @@ -1235,7 +1236,10 @@ export const sourceWizardLogic = kea<sourceWizardLogicType>([
actions.onNext()
},
selectConnector: () => {
actions.addProductIntent({ product_type: ProductKey.DATA_WAREHOUSE, intent_context: 'selected connector' })
actions.addProductIntent({
product_type: ProductKey.DATA_WAREHOUSE,
intent_context: ProductIntentContext.SELECTED_CONNECTOR,
})
},
})),
urlToAction(({ actions }) => ({
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/scenes/experiments/experimentLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { hasFormErrors, toParams } from 'lib/utils'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { ProductIntentContext } from 'lib/utils/product-intents'
import { addProjectIdIfMissing } from 'lib/utils/router-utils'
import {
indexToVariantKeyFeatureFlagPayloads,
Expand Down Expand Up @@ -641,7 +642,10 @@ export const experimentLogic = kea<experimentLogicType>([
})
if (response) {
actions.reportExperimentCreated(response)
actions.addProductIntent({ product_type: ProductKey.EXPERIMENTS })
actions.addProductIntent({
product_type: ProductKey.EXPERIMENTS,
intent_context: ProductIntentContext.EXPERIMENT_CREATED,
})
}
}
} catch (error: any) {
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/scenes/feature-flags/featureFlagLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagL
import { sum, toParams } from 'lib/utils'
import { deleteWithUndo } from 'lib/utils/deleteWithUndo'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { ProductIntentContext } from 'lib/utils/product-intents'
import { NEW_EARLY_ACCESS_FEATURE } from 'products/early_access_features/frontend/earlyAccessFeatureLogic'
import { dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic'
import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic'
Expand Down Expand Up @@ -586,7 +587,10 @@ export const featureFlagLogic = kea<featureFlagLogicType>([
if (values.roleBasedAccessEnabled && savedFlag.id) {
featureFlagPermissionsLogic({ flagId: null })?.actions.addAssociatedRoles(savedFlag.id)
}
actions.addProductIntent({ product_type: ProductKey.FEATURE_FLAGS })
actions.addProductIntent({
product_type: ProductKey.FEATURE_FLAGS,
intent_context: ProductIntentContext.FEATURE_FLAG_CREATED,
})
} else {
savedFlag = await api.update(
`api/projects/${values.currentProjectId}/feature_flags/${updatedFlag.id}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IconCheck, IconMap, IconMessage, IconStack } from '@posthog/icons'
import { LemonButton, Link, Spinner } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { WavingHog } from 'lib/components/hedgehogs'
import { ProductIntentContext } from 'lib/utils/product-intents'
import React from 'react'
import { convertLargeNumberToWords } from 'scenes/billing/billing-utils'
import { billingProductLogic } from 'scenes/billing/billingProductLogic'
Expand Down Expand Up @@ -65,7 +66,7 @@ const GetStartedButton = ({ product }: { product: BillingProductV2Type }): JSX.E
setTeamPropertiesForProduct(product.type as ProductKey)
addProductIntent({
product_type: product.type as ProductKey,
intent_context: 'onboarding product selected - primary',
intent_context: ProductIntentContext.ONBOARDING_PRODUCT_SELECTED_PRIMARY,
})
goToNextStep()
}}
Expand All @@ -85,7 +86,7 @@ const GetStartedButton = ({ product }: { product: BillingProductV2Type }): JSX.E
setTeamPropertiesForProduct(product.type as ProductKey)
addProductIntent({
product_type: product.type as ProductKey,
intent_context: 'onboarding product selected - secondary',
intent_context: ProductIntentContext.ONBOARDING_PRODUCT_SELECTED_SECONDARY,
})
completeOnboarding()
}}
Expand All @@ -100,7 +101,7 @@ const GetStartedButton = ({ product }: { product: BillingProductV2Type }): JSX.E
setTeamPropertiesForProduct(product.type as ProductKey)
addProductIntent({
product_type: product.type as ProductKey,
intent_context: 'onboarding product selected - secondary',
intent_context: ProductIntentContext.ONBOARDING_PRODUCT_SELECTED_SECONDARY,
})
goToNextStep()
}}
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/scenes/products/productsLogic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { actions, connect, kea, listeners, path, reducers } from 'kea'
import { router } from 'kea-router'
import { ProductIntentContext } from 'lib/utils/product-intents'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'

Expand Down Expand Up @@ -46,8 +47,8 @@ export const productsLogic = kea<productsLogicType>([
product_type: productKey as ProductKey,
intent_context:
values.firstProductOnboarding === productKey
? 'onboarding product selected - primary'
: 'onboarding product selected - secondary',
? ProductIntentContext.ONBOARDING_PRODUCT_SELECTED_PRIMARY
: ProductIntentContext.ONBOARDING_PRODUCT_SELECTED_SECONDARY,
})
})
},
Expand Down
20 changes: 9 additions & 11 deletions frontend/src/scenes/teamLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { identifierToHuman, isUserLoggedIn, resolveWebhookService } from 'lib/utils'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { getAppContext } from 'lib/utils/getAppContext'
import {
addProductIntent,
addProductIntentForCrossSell,
type ProductCrossSellProperties,
type ProductIntentProperties,
} from 'lib/utils/product-intents'

import { CorrelationConfigType, ProductKey, ProjectType, TeamPublicType, TeamType } from '~/types'

Expand Down Expand Up @@ -148,17 +154,9 @@ export const teamLogic = kea<teamLogicType>([
/**
* If adding a product intent that also represents regular product usage, see explainer in posthog.models.product_intent.product_intent.py.
*/
addProductIntent: async ({
product_type,
intent_context,
}: {
product_type: ProductKey
intent_context?: string | null
}) =>
await api.update(`api/environments/${values.currentTeamId}/add_product_intent`, {
product_type,
intent_context: intent_context ?? undefined,
}),
addProductIntent: async (properties: ProductIntentProperties) => await addProductIntent(properties),
addProductIntentForCrossSell: async (properties: ProductCrossSellProperties) =>
await addProductIntentForCrossSell(properties),
recordProductIntentOnboardingComplete: async ({ product_type }: { product_type: ProductKey }) =>
await api.update(`api/environments/${values.currentTeamId}/complete_product_onboarding`, {
product_type,
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/scenes/web-analytics/tiles/WebAnalyticsTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IconOpenInNew, IconTrendingDown, IconTrendingFlat } from 'lib/lemon-ui/
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch'
import { percentage, UnexpectedNeverError } from 'lib/utils'
import { ProductCrossSellLocation, trackProductCrossSell } from 'lib/utils/cross-sell'
import { addProductIntentForCrossSell, ProductIntentContext } from 'lib/utils/product-intents'
import { useCallback, useMemo } from 'react'
import { NewActionButton } from 'scenes/actions/NewActionButton'
import { countryCodeToFlag, countryCodeToName } from 'scenes/insights/views/WorldMap'
Expand Down Expand Up @@ -755,11 +755,10 @@ const RenderReplayButton = ({
targetBlank: true,
onClick: (e: React.MouseEvent) => {
e.stopPropagation()
trackProductCrossSell({
void addProductIntentForCrossSell({
from: ProductKey.WEB_ANALYTICS,
to: ProductKey.SESSION_REPLAY,
location: ProductCrossSellLocation.WEB_ANALYTICS_INSIGHT,
context: {},
intent_context: ProductIntentContext.WEB_ANALYTICS_INSIGHT,
})
},
}
Expand Down
5 changes: 5 additions & 0 deletions posthog/api/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,10 +629,14 @@ def add_product_intent(self, request: request.Request, *args, **kwargs):
current_url = request.headers.get("Referer")
session_id = request.headers.get("X-Posthog-Session-Id")
should_report_product_intent = False
metadata = request.data.get("metadata", {})

if not product_type:
return response.Response({"error": "product_type is required"}, status=400)

if not isinstance(metadata, dict):
return response.Response({"error": "'metadata' must be a dictionary"}, status=400)

product_intent, created = ProductIntent.objects.get_or_create(team=team, product_type=product_type)

if created:
Expand All @@ -654,6 +658,7 @@ def add_product_intent(self, request: request.Request, *args, **kwargs):
user,
"user showed product intent",
{
**metadata,
"product_key": product_type,
"$set_once": {"first_onboarding_product_selected": product_type},
"$current_url": current_url,
Expand Down

0 comments on commit 63afbcc

Please sign in to comment.