Skip to content

Commit a08a5cd

Browse files
committed
feat(surveys): add cross-sell to funnel insights
1 parent 0f7236d commit a08a5cd

File tree

7 files changed

+70
-3
lines changed

7 files changed

+70
-3
lines changed

frontend/src/lib/components/Cards/InsightCard/InsightCard.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ export interface InsightCardProps extends Resizeable {
9494
style?: React.CSSProperties
9595
children?: React.ReactNode
9696
tile?: DashboardTile<QueryBasedInsightModel>
97+
/** survey opportunity for this insight */
98+
surveyOpportunity?: boolean
9799
}
98100

99101
function InsightCardInternal(
@@ -131,6 +133,7 @@ function InsightCardInternal(
131133
children,
132134
breakdownColorOverride: _breakdownColorOverride,
133135
dataColorThemeId: _dataColorThemeId,
136+
surveyOpportunity,
134137
...divProps
135138
}: InsightCardProps,
136139
ref: React.Ref<HTMLDivElement>
@@ -251,6 +254,7 @@ function InsightCardInternal(
251254
filtersOverride={filtersOverride}
252255
variablesOverride={variablesOverride}
253256
placement={placement}
257+
surveyOpportunity={surveyOpportunity}
254258
/>
255259
<div className="InsightCard__viz">
256260
{BlockingEmptyState ? (

frontend/src/lib/components/Cards/InsightCard/InsightMeta.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { insightLogic } from 'scenes/insights/insightLogic'
2828
import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic'
2929
import { useSummarizeInsight } from 'scenes/insights/summarizeInsight'
3030
import { getOverrideWarningPropsForButton } from 'scenes/insights/utils'
31+
import { SurveyOpportunityButton } from 'scenes/surveys/components/SurveyOpportunityButton'
3132
import { urls } from 'scenes/urls'
3233

3334
import { dashboardsModel } from '~/models/dashboardsModel'
@@ -67,6 +68,7 @@ interface InsightMetaProps
6768
| 'filtersOverride'
6869
| 'variablesOverride'
6970
| 'placement'
71+
| 'surveyOpportunity'
7072
> {
7173
tile?: DashboardTile<QueryBasedInsightModel>
7274
insight: QueryBasedInsightModel
@@ -98,6 +100,7 @@ export function InsightMeta({
98100
showDetailsControls = true,
99101
moreButtons,
100102
placement,
103+
surveyOpportunity,
101104
}: InsightMetaProps): JSX.Element {
102105
const { short_id, name, dashboards, next_allowed_client_refresh: nextAllowedClientRefresh } = insight
103106
const { insightProps, insightFeedback } = useValues(insightLogic)
@@ -156,6 +159,13 @@ export function InsightMeta({
156159
</div>
157160
) : null
158161

162+
const surveyOpportunityButton =
163+
surveyOpportunity && featureFlags[FEATURE_FLAGS.SURVEYS_FUNNELS_CROSS_SELL] ? (
164+
<div className="flex">
165+
<SurveyOpportunityButton insight={insight} />
166+
</div>
167+
) : null
168+
159169
// If user can't view the insight, show minimal interface
160170
if (!canViewInsight) {
161171
return (
@@ -415,7 +425,7 @@ export function InsightMeta({
415425
moreTooltip={
416426
canEditInsight ? 'Rename, duplicate, export, refresh and more…' : 'Duplicate, export, refresh and more…'
417427
}
418-
extraControls={feedbackButtons}
428+
extraControls={surveyOpportunityButton ?? feedbackButtons}
419429
/>
420430
)
421431
}

frontend/src/lib/constants.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ export const FEATURE_FLAGS = {
342342
EXPERIMENTS_USE_NEW_CREATE_FORM: 'experiments-use-new-create-form', // owner: @rodrigoi #team-experiments
343343
EXPERIMENTS_NEW_CALCULATOR: 'experiments-new-calculator', // owner: @jurajmajerik #team-experiments
344344
AGENT_MODES: 'phai-agent-modes', // owner: @skoob13 #team-posthog-ai
345+
SURVEYS_FUNNELS_CROSS_SELL: 'survey-funnels-cross-sell', // owner: @adboio #team-surveys
345346
} as const
346347
export type FeatureFlagLookupKey = keyof typeof FEATURE_FLAGS
347348
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]

frontend/src/scenes/dashboard/DashboardItems.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
1515
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
1616
import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
1717
import { BREAKPOINTS, BREAKPOINT_COLUMN_COUNTS } from 'scenes/dashboard/dashboardUtils'
18+
import { useSurveyLinkedInsights } from 'scenes/surveys/hooks/useSurveyLinkedInsights'
19+
import { getBestSurveyOpportunityFunnel } from 'scenes/surveys/utils/opportunityDetection'
1820
import { urls } from 'scenes/urls'
1921

2022
import { getCurrentExporterData } from '~/exporter/exporterViewLogic'
@@ -54,6 +56,11 @@ export function DashboardItems(): JSX.Element {
5456
const { nameSortedDashboards } = useValues(dashboardsModel)
5557
const otherDashboards = nameSortedDashboards.filter((nsdb) => nsdb.id !== dashboard?.id)
5658
const { featureFlags } = useValues(featureFlagLogic)
59+
const { data: surveyLinkedInsights, loading: surveyLinkedInsightsLoading } = useSurveyLinkedInsights()
60+
61+
const bestSurveyOpportunityFunnel = surveyLinkedInsightsLoading
62+
? null
63+
: getBestSurveyOpportunityFunnel(tiles || [], surveyLinkedInsights)
5764

5865
const [resizingItem, setResizingItem] = useState<any>(null)
5966

@@ -187,6 +194,7 @@ export function DashboardItems(): JSX.Element {
187194
// :HACKY: The two props below aren't actually used in the component, but are needed to trigger a re-render
188195
breakdownColorOverride={temporaryBreakdownColors}
189196
dataColorThemeId={dataColorThemeId}
197+
surveyOpportunity={tile.id === bestSurveyOpportunityFunnel?.id}
190198
{...commonTileProps}
191199
// NOTE: ReactGridLayout additionally injects its resize handles as `children`!
192200
/>

frontend/src/scenes/surveys/components/SurveyOpportunityButton.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { ProductIntentContext, addProductIntent } from 'lib/utils/product-intent
1010
import { useMaxTool } from 'scenes/max/useMaxTool'
1111
import { urls } from 'scenes/urls'
1212

13-
import { ProductKey, QueryBasedInsightModel } from '~/types'
13+
import { ProductKey } from '~/queries/schema/schema-general'
14+
import { QueryBasedInsightModel } from '~/types'
1415

1516
import { SURVEY_CREATED_SOURCE } from '../constants'
1617
import { captureMaxAISurveyCreationException } from '../utils'
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import posthog from 'posthog-js'
2+
import { useEffect, useMemo, useState } from 'react'
3+
4+
import api from 'lib/api'
5+
6+
import { Survey } from '~/types'
7+
8+
/**
9+
* Fetch all insight IDs which are already linked to surveys. This is separate
10+
* from surveysLogic to avoid side effects (e.g. product intent) in unrelated
11+
* components, like dashboards
12+
*
13+
* @returns set of insight IDs already linked to existing surveys
14+
*/
15+
export function useSurveyLinkedInsights(): {
16+
loading: boolean
17+
data: Set<number>
18+
} {
19+
const [surveys, setSurveys] = useState<Survey[]>([])
20+
const [loading, setLoading] = useState<boolean>(true)
21+
22+
useEffect(() => {
23+
api.surveys
24+
.list()
25+
.then((response) => setSurveys(response.results))
26+
.catch((error) => {
27+
posthog.captureException(error, {
28+
action: 'fetch-survey-linked-insights',
29+
})
30+
})
31+
.finally(() => setLoading(false))
32+
}, [])
33+
34+
return {
35+
loading,
36+
data: useMemo(
37+
() => new Set(surveys.map((survey) => survey.linked_insight_id).filter((id): id is number => id != null)),
38+
[surveys]
39+
),
40+
}
41+
}

posthog/migrations/0915_alter_survey_linked_insight.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
# manually updated to pass CI checks, using SeparateDatabaseAndState
88
class Migration(migrations.Migration):
9+
atomic = False # Required for CREATE INDEX CONCURRENTLY
10+
911
dependencies = [
1012
("posthog", "0914_alter_userproductlist_reason"),
1113
]
@@ -29,7 +31,7 @@ class Migration(migrations.Migration):
2931
database_operations=[
3032
migrations.RunSQL(
3133
"""
32-
CREATE INDEX IF NOT EXISTS "posthog_survey_linked_insight_id_586524f3" ON "posthog_survey" ("linked_insight_id"); -- existing-table-constraint-ignore
34+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "posthog_survey_linked_insight_id_586524f3" ON "posthog_survey" ("linked_insight_id"); -- existing-table-constraint-ignore
3335
""",
3436
reverse_sql="""
3537
DROP INDEX IF EXISTS "posthog_survey_linked_insight_id_586524f3";

0 commit comments

Comments
 (0)