Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(data exploration): convert funnel correlation feedback #14971

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { expectLogic } from 'kea-test-utils'
import { initKeaTests } from '~/test/init'
import { FunnelCorrelationResultsType, FunnelCorrelationType, InsightLogicProps, InsightType } from '~/types'

import { funnelCorrelationDetailsLogic } from './funnelCorrelationDetailsLogic'

const funnelResults = [
{
action_id: '$pageview',
count: 19,
name: '$pageview',
order: 0,
type: 'events',
},
{
action_id: '$pageview',
count: 7,
name: '$pageview',
order: 1,
type: 'events',
},
{
action_id: '$pageview',
count: 4,
name: '$pageview',
order: 2,
type: 'events',
},
]

describe('funnelCorrelationDetailsLogic', () => {
let logic: ReturnType<typeof funnelCorrelationDetailsLogic.build>

beforeEach(() => {
initKeaTests(false)
})

const defaultProps: InsightLogicProps = {
dashboardItemId: undefined,
cachedInsight: {
short_id: undefined,
filters: {
insight: InsightType.FUNNELS,
actions: [
{ id: '$pageview', order: 0 },
{ id: '$pageview', order: 1 },
],
},
result: funnelResults,
},
}

beforeEach(async () => {
logic = funnelCorrelationDetailsLogic(defaultProps)
logic.mount()
})

describe('correlationMatrixAndScore', () => {
it('returns calculated values based on selected details', async () => {
await expectLogic(logic, () =>
logic.actions.setFunnelCorrelationDetails({
event: { event: 'some event', elements: [], properties: {} },
success_people_url: '',
failure_people_url: '',
success_count: 2,
failure_count: 4,
odds_ratio: 3,
correlation_type: FunnelCorrelationType.Success,
result_type: FunnelCorrelationResultsType.Events,
})
).toMatchValues({
correlationMatrixAndScore: {
correlationScore: expect.anything(),
correlationScoreStrength: 'weak',
truePositive: 2,
falsePositive: 2,
trueNegative: 11,
falseNegative: 4,
},
})

expect(logic.values.correlationMatrixAndScore.correlationScore).toBeCloseTo(0.204)
})
})
})
105 changes: 105 additions & 0 deletions frontend/src/scenes/funnels/funnelCorrelationDetailsLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { kea, props, key, path, connect, selectors, reducers, actions } from 'kea'
import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils'
import { FunnelCorrelation, InsightLogicProps } from '~/types'

import { insightLogic } from 'scenes/insights/insightLogic'
import { funnelLogic } from './funnelLogic'
import { funnelDataLogic } from './funnelDataLogic'

import type { funnelCorrelationDetailsLogicType } from './funnelCorrelationDetailsLogicType'

export const funnelCorrelationDetailsLogic = kea<funnelCorrelationDetailsLogicType>([
props({} as InsightLogicProps),
key(keyForInsightLogicProps('insight_funnel')),
path((key) => ['scenes', 'funnels', 'funnelCorrelationDetailsLogic', key]),
connect((props: InsightLogicProps) => ({
values: [
insightLogic(props),
['isUsingDataExploration'],
funnelLogic(props),
['steps as legacySteps'],
funnelDataLogic(props),
['steps as dataExplorationSteps'],
],
})),

actions({
setFunnelCorrelationDetails: (payload: FunnelCorrelation | null) => ({ payload }),
}),

reducers({
funnelCorrelationDetails: [
null as null | FunnelCorrelation,
{
setFunnelCorrelationDetails: (_, { payload }) => payload,
},
],
}),

selectors({
steps: [
(s) => [s.isUsingDataExploration, s.dataExplorationSteps, s.legacySteps],
(isUsingDataExploration, dataExplorationApiParams, legacyApiParams) => {
return isUsingDataExploration ? dataExplorationApiParams : legacyApiParams
},
],

correlationMatrixAndScore: [
(s) => [s.funnelCorrelationDetails, s.steps],
(
funnelCorrelationDetails,
steps
): {
truePositive: number
falsePositive: number
trueNegative: number
falseNegative: number
correlationScore: number
correlationScoreStrength: 'weak' | 'moderate' | 'strong' | null
} => {
if (!funnelCorrelationDetails) {
return {
truePositive: 0,
falsePositive: 0,
trueNegative: 0,
falseNegative: 0,
correlationScore: 0,
correlationScoreStrength: null,
}
}

const successTotal = steps[steps.length - 1].count
const failureTotal = steps[0].count - successTotal
const success = funnelCorrelationDetails.success_count
const failure = funnelCorrelationDetails.failure_count

const truePositive = success // has property, converted
const falseNegative = failure // has property, but dropped off
const trueNegative = failureTotal - failure // doesn't have property, dropped off
const falsePositive = successTotal - success // doesn't have property, converted

// Phi coefficient: https://en.wikipedia.org/wiki/Phi_coefficient
const correlationScore =
(truePositive * trueNegative - falsePositive * falseNegative) /
Math.sqrt(
(truePositive + falsePositive) *
(truePositive + falseNegative) *
(trueNegative + falsePositive) *
(trueNegative + falseNegative)
)

const correlationScoreStrength =
Math.abs(correlationScore) > 0.5 ? 'strong' : Math.abs(correlationScore) > 0.3 ? 'moderate' : 'weak'

return {
correlationScore,
truePositive,
falsePositive,
trueNegative,
falseNegative,
correlationScoreStrength,
}
},
],
}),
])
109 changes: 109 additions & 0 deletions frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import posthog from 'posthog-js'
import { expectLogic } from 'kea-test-utils'
import { initKeaTests } from '~/test/init'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { AvailableFeature, InsightLogicProps, InsightType } from '~/types'
import { useAvailableFeatures } from '~/mocks/features'
import { funnelCorrelationFeedbackLogic } from './funnelCorrelationFeedbackLogic'

describe('funnelCorrelationFeedbackLogic', () => {
let logic: ReturnType<typeof funnelCorrelationFeedbackLogic.build>

beforeEach(() => {
useAvailableFeatures([AvailableFeature.CORRELATION_ANALYSIS])
initKeaTests(false)
})

const defaultProps: InsightLogicProps = {
dashboardItemId: undefined,
cachedInsight: {
short_id: undefined,
filters: {
insight: InsightType.FUNNELS,
actions: [
{ id: '$pageview', order: 0 },
{ id: '$pageview', order: 1 },
],
},
result: [],
},
}

beforeEach(async () => {
logic = funnelCorrelationFeedbackLogic(defaultProps)
logic.mount()
})

it('opens detailed feedback on selecting a valid rating', async () => {
await expectLogic(logic, () => {
logic.actions.setCorrelationFeedbackRating(1)
})
.toMatchValues(logic, {
correlationFeedbackRating: 1,
})
.toDispatchActions(logic, [
(action) =>
action.type === logic.actionTypes.setCorrelationDetailedFeedbackVisible &&
action.payload.visible === true,
])
.toMatchValues(logic, {
correlationDetailedFeedbackVisible: true,
})
})

it('doesnt opens detailed feedback on selecting an invalid rating', async () => {
await expectLogic(logic, () => {
logic.actions.setCorrelationFeedbackRating(0)
})
.toMatchValues(logic, {
correlationFeedbackRating: 0,
})
.toDispatchActions(logic, [
(action) =>
action.type === logic.actionTypes.setCorrelationDetailedFeedbackVisible &&
action.payload.visible === false,
])
.toMatchValues(logic, {
correlationDetailedFeedbackVisible: false,
})
})

it('captures emoji feedback properly', async () => {
jest.spyOn(posthog, 'capture')
await expectLogic(logic, () => {
logic.actions.setCorrelationFeedbackRating(1)
})
.toMatchValues(logic, {
// reset after sending feedback
correlationFeedbackRating: 1,
})
.toDispatchActions(eventUsageLogic, ['reportCorrelationAnalysisFeedback'])

expect(posthog.capture).toBeCalledWith('correlation analysis feedback', { rating: 1 })
})

it('goes away on sending feedback, capturing it properly', async () => {
jest.spyOn(posthog, 'capture')
await expectLogic(logic, () => {
logic.actions.setCorrelationFeedbackRating(2)
logic.actions.setCorrelationDetailedFeedback('tests')
logic.actions.sendCorrelationAnalysisFeedback()
})
.toMatchValues(logic, {
// reset after sending feedback
correlationFeedbackRating: 0,
correlationDetailedFeedback: '',
correlationFeedbackHidden: true,
})
.toDispatchActions(eventUsageLogic, ['reportCorrelationAnalysisDetailedFeedback'])
.toFinishListeners()

await expectLogic(eventUsageLogic).toFinishListeners()

expect(posthog.capture).toBeCalledWith('correlation analysis feedback', { rating: 2 })
expect(posthog.capture).toBeCalledWith('correlation analysis detailed feedback', {
rating: 2,
comments: 'tests',
})
})
})
81 changes: 81 additions & 0 deletions frontend/src/scenes/funnels/funnelCorrelationFeedbackLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { actions, connect, kea, key, listeners, path, props, reducers } from 'kea'
import { lemonToast } from '@posthog/lemon-ui'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils'

import type { funnelCorrelationFeedbackLogicType } from './funnelCorrelationFeedbackLogicType'
import { InsightLogicProps } from '~/types'
import { funnelCorrelationLogic } from './funnelCorrelationLogic'
import { funnelPropertyCorrelationLogic } from './funnelPropertyCorrelationLogic'

export const funnelCorrelationFeedbackLogic = kea<funnelCorrelationFeedbackLogicType>([
props({} as InsightLogicProps),
key(keyForInsightLogicProps('insight_funnel')),
path((key) => ['scenes', 'funnels', 'funnelCorrelationFeedbackLogic', key]),

connect((props: InsightLogicProps) => ({
actions: [
funnelCorrelationLogic(props),
['loadEventCorrelations'],
funnelPropertyCorrelationLogic(props),
['loadPropertyCorrelations'],
],
})),

actions({
sendCorrelationAnalysisFeedback: true,
hideCorrelationAnalysisFeedback: true,
setCorrelationFeedbackRating: (rating: number) => ({ rating }),
setCorrelationDetailedFeedback: (comment: string) => ({ comment }),
setCorrelationDetailedFeedbackVisible: (visible: boolean) => ({ visible }),
}),
reducers({
correlationFeedbackHidden: [
true,
{
// don't load the feedback form until after some results were loaded
loadEventCorrelations: () => false,
loadPropertyCorrelations: () => false,
sendCorrelationAnalysisFeedback: () => true,
hideCorrelationAnalysisFeedback: () => true,
},
],
correlationDetailedFeedbackVisible: [
false,
{
setCorrelationDetailedFeedbackVisible: (_, { visible }) => visible,
},
],
correlationFeedbackRating: [
0,
{
setCorrelationFeedbackRating: (_, { rating }) => rating,
},
],
correlationDetailedFeedback: [
'',
{
setCorrelationDetailedFeedback: (_, { comment }) => comment,
},
],
}),
listeners(({ actions, values }) => ({
sendCorrelationAnalysisFeedback: () => {
eventUsageLogic.actions.reportCorrelationAnalysisDetailedFeedback(
values.correlationFeedbackRating,
values.correlationDetailedFeedback
)
actions.setCorrelationFeedbackRating(0)
actions.setCorrelationDetailedFeedback('')
lemonToast.success('Thanks for your feedback! Your comments help us improve')
},
setCorrelationFeedbackRating: ({ rating }) => {
const feedbackBoxVisible = rating > 0
actions.setCorrelationDetailedFeedbackVisible(feedbackBoxVisible)
if (feedbackBoxVisible) {
// Don't send event when resetting reducer
eventUsageLogic.actions.reportCorrelationAnalysisFeedback(rating)
}
},
})),
])
Loading