Skip to content

Commit

Permalink
Add custom bounce rate control
Browse files Browse the repository at this point in the history
  • Loading branch information
robbie-c committed Dec 21, 2024
1 parent 18198bb commit 6b55da2
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 11 deletions.
3 changes: 3 additions & 0 deletions frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7359,6 +7359,9 @@
"additionalProperties": false,
"description": "HogQL Query Options are automatically set per team. However, they can be overridden in the query.",
"properties": {
"bounceRateDurationSeconds": {
"type": "number"
},
"bounceRatePageViewMode": {
"enum": ["count_pageviews", "uniq_urls", "uniq_page_screen_autocaptures"],
"type": "string"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/queries/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export interface HogQLQueryModifiers {
s3TableUseInvalidColumns?: boolean
personsJoinMode?: 'inner' | 'left'
bounceRatePageViewMode?: 'count_pageviews' | 'uniq_urls' | 'uniq_page_screen_autocaptures'
bounceRateDurationSeconds?: number
sessionTableVersion?: 'auto' | 'v1' | 'v2'
propertyGroupsMode?: 'enabled' | 'disabled' | 'optimized'
useMaterializedViews?: boolean
Expand Down
18 changes: 12 additions & 6 deletions frontend/src/scenes/settings/SettingsMap.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BounceRateDurationSetting } from 'scenes/settings/environment/BounceRateDuration'
import { BounceRatePageViewModeSetting } from 'scenes/settings/environment/BounceRatePageViewMode'
import { CookielessServerHashModeSetting } from 'scenes/settings/environment/CookielessServerHashMode'
import { CustomChannelTypes } from 'scenes/settings/environment/CustomChannelTypes'
Expand Down Expand Up @@ -194,12 +195,6 @@ export const SETTINGS_MAP: SettingSection[] = [
component: <PersonsJoinMode />,
flag: 'SETTINGS_PERSONS_JOIN_MODE',
},
{
id: 'bounce-rate-page-view-mode',
title: 'Bounce rate page view mode',
component: <BounceRatePageViewModeSetting />,
flag: 'SETTINGS_BOUNCE_RATE_PAGE_VIEW_MODE',
},
{
id: 'session-table-version',
title: 'Sessions Table Version',
Expand All @@ -225,6 +220,17 @@ export const SETTINGS_MAP: SettingSection[] = [
component: <CookielessServerHashModeSetting />,
flag: 'COOKIELESS_SERVER_HASH_MODE_SETTING',
},
{
id: 'bounce-rate-duration',
title: 'Bounce rate duration',
component: <BounceRateDurationSetting />,
},
{
id: 'bounce-rate-page-view-mode',
title: 'Bounce rate page view mode',
component: <BounceRatePageViewModeSetting />,
flag: 'SETTINGS_BOUNCE_RATE_PAGE_VIEW_MODE',
},
],
},

Expand Down
34 changes: 34 additions & 0 deletions frontend/src/scenes/settings/environment/BounceRateDuration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useActions, useValues } from 'kea'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonInput } from 'lib/lemon-ui/LemonInput'
import { useState } from 'react'
import { teamLogic } from 'scenes/teamLogic'

export function BounceRateDurationSetting(): JSX.Element {
const { updateCurrentTeam } = useActions(teamLogic)
const { currentTeam } = useValues(teamLogic)

const savedDuration =
currentTeam?.modifiers?.bounceRateDurationSeconds ?? currentTeam?.default_modifiers?.bounceRateDurationSeconds
const [bounceRateDuration, setBounceRateDuration] = useState<number | undefined>(savedDuration)

const handleChange = (duration: number | undefined): void => {
updateCurrentTeam({ modifiers: { ...currentTeam?.modifiers, bounceRateDurationSeconds: duration } })
}

return (
<>
<p>Choose how long a user can stay on a page, before the session is not a bounce.</p>
<LemonInput type="number" min={1} max={120} value={bounceRateDuration} onChange={setBounceRateDuration} />
<div className="mt-4">
<LemonButton
type="primary"
onClick={() => handleChange(bounceRateDuration)}
disabledReason={bounceRateDuration === savedDuration ? 'No changes to save' : undefined}
>
Save
</LemonButton>
</div>
</>
)
}
1 change: 1 addition & 0 deletions frontend/src/scenes/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export type SettingId =
| 'hedgehog-mode'
| 'persons-join-mode'
| 'bounce-rate-page-view-mode'
| 'bounce-rate-duration'
| 'session-table-version'
| 'web-vitals-autocapture'
| 'dead-clicks-autocapture'
Expand Down
11 changes: 10 additions & 1 deletion posthog/hogql/database/schema/sessions_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ChannelTypeExprs,
DEFAULT_CHANNEL_TYPES,
)
from posthog.hogql.database.schema.sessions_v2 import DEFAULT_BOUNCE_RATE_DURATION_SECONDS
from posthog.hogql.database.schema.util.where_clause_extractor import SessionMinTimestampWhereClauseExtractorV1
from posthog.hogql.errors import ResolutionError
from posthog.models.property_definition import PropertyType
Expand Down Expand Up @@ -205,6 +206,11 @@ def arg_max_merge_field(field_name: str) -> ast.Call:
args=[aggregate_fields["$urls"]],
)

bounce_rate_duration_seconds = (
context.modifiers.bounceRateDurationSeconds
if context.modifiers.bounceRateDurationSeconds is not None
else DEFAULT_BOUNCE_RATE_DURATION_SECONDS
)
if context.modifiers.bounceRatePageViewMode == BounceRatePageViewMode.UNIQ_URLS:
bounce_pageview_count = aggregate_fields["$num_uniq_urls"]
else:
Expand All @@ -230,7 +236,10 @@ def arg_max_merge_field(field_name: str) -> ast.Call:
# if session duration >= 10 seconds, not a bounce
ast.Call(
name="greaterOrEquals",
args=[aggregate_fields["$session_duration"], ast.Constant(value=10)],
args=[
aggregate_fields["$session_duration"],
ast.Constant(value=bounce_rate_duration_seconds),
],
),
],
)
Expand Down
18 changes: 16 additions & 2 deletions posthog/hogql/database/schema/sessions_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
if TYPE_CHECKING:
from posthog.models.team import Team


DEFAULT_BOUNCE_RATE_DURATION_SECONDS = 30

RAW_SESSIONS_FIELDS: dict[str, FieldOrTable] = {
"session_id_v7": IntegerDatabaseField(name="session_id_v7"),
"team_id": IntegerDatabaseField(name="team_id"),
Expand Down Expand Up @@ -252,6 +255,11 @@ def arg_max_merge_field(field_name: str) -> ast.Call:
args=[aggregate_fields["$urls"]],
)

bounce_rate_duration_seconds = (
context.modifiers.bounceRateDurationSeconds
if context.modifiers.bounceRateDurationSeconds is not None
else DEFAULT_BOUNCE_RATE_DURATION_SECONDS
)
if context.modifiers.bounceRatePageViewMode == BounceRatePageViewMode.UNIQ_PAGE_SCREEN_AUTOCAPTURES:
bounce_event_count = aggregate_fields["$page_screen_autocapture_count_up_to"]
aggregate_fields["$is_bounce"] = ast.Call(
Expand All @@ -271,7 +279,10 @@ def arg_max_merge_field(field_name: str) -> ast.Call:
# if session duration >= 10 seconds, not a bounce
ast.Call(
name="greaterOrEquals",
args=[aggregate_fields["$session_duration"], ast.Constant(value=10)],
args=[
aggregate_fields["$session_duration"],
ast.Constant(value=bounce_rate_duration_seconds),
],
),
],
)
Expand Down Expand Up @@ -302,7 +313,10 @@ def arg_max_merge_field(field_name: str) -> ast.Call:
# if session duration >= 10 seconds, not a bounce
ast.Call(
name="greaterOrEquals",
args=[aggregate_fields["$session_duration"], ast.Constant(value=10)],
args=[
aggregate_fields["$session_duration"],
ast.Constant(value=bounce_rate_duration_seconds),
],
),
],
)
Expand Down
62 changes: 60 additions & 2 deletions posthog/hogql/database/schema/test/test_sessions_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@


class TestSessionsV2(ClickhouseTestMixin, APIBaseTest):
def __execute(self, query, bounce_rate_mode=BounceRatePageViewMode.COUNT_PAGEVIEWS):
def __execute(self, query, bounce_rate_mode=BounceRatePageViewMode.COUNT_PAGEVIEWS, bounce_rate_duration=None):
modifiers = HogQLQueryModifiers(
sessionTableVersion=SessionTableVersion.V2, bounceRatePageViewMode=bounce_rate_mode
sessionTableVersion=SessionTableVersion.V2,
bounceRatePageViewMode=bounce_rate_mode,
bounce_rate_duration=bounce_rate_duration,
)
return execute_hogql_query(
query=query,
Expand Down Expand Up @@ -515,6 +517,62 @@ def test_bounce_rate(self, bounce_rate_mode):
(0, s5),
]

@parameterized.expand(
[[BounceRatePageViewMode.UNIQ_PAGE_SCREEN_AUTOCAPTURES], [BounceRatePageViewMode.COUNT_PAGEVIEWS]]
)
def test_custom_bounce_rate_duration(self, bounce_rate_mode):
time = time_ns() // (10**6)
# ensure the sessions ids are sortable by giving them different time components
s = str(uuid7(time))

# 15 second session with a pageleave
_create_event(
event="$pageview",
team=self.team,
distinct_id="d4",
properties={"$session_id": s, "$current_url": "https://example.com/6"},
timestamp="2023-12-11T12:00:00",
)
_create_event(
event="$pageleave",
team=self.team,
distinct_id="d4",
properties={"$session_id": s, "$current_url": "https://example.com/6"},
timestamp="2023-12-11T12:00:15",
)

# with default settings this should not be a bounce
assert (
self.__execute(
parse_select(
"select $is_bounce, session_id from sessions ORDER BY session_id",
),
bounce_rate_mode=bounce_rate_mode,
)
) == [(0, s)]

# with a custom 10 second duration this should be a bounce
assert (
self.__execute(
parse_select(
"select $is_bounce, session_id from sessions ORDER BY session_id",
),
bounce_rate_mode=bounce_rate_mode,
bounce_rate_duration=10,
)
) == [(1, s)]

# with a custom 30 second duration this should be a bounce
assert (
self.__execute(
parse_select(
"select $is_bounce, session_id from sessions ORDER BY session_id",
),
bounce_rate_mode=bounce_rate_mode,
bounce_rate_duration=60,
)
) == [(1, s)]

def test_last_external_click_url(self):
s1 = str(uuid7())

Expand Down
1 change: 1 addition & 0 deletions posthog/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2262,6 +2262,7 @@ class HogQLQueryModifiers(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
bounceRateDurationSeconds: Optional[float] = None
bounceRatePageViewMode: Optional[BounceRatePageViewMode] = None
customChannelTypeRules: Optional[list[CustomChannelRule]] = None
dataWarehouseEventsModifiers: Optional[list[DataWarehouseEventsModifier]] = None
Expand Down

0 comments on commit 6b55da2

Please sign in to comment.