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(web-analytics): Add custom bounce rate control #27121

Merged
merged 11 commits into from
Dec 26, 2024
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.
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.
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.
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.
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.
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.
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.
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.
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>
robbie-c marked this conversation as resolved.
Show resolved Hide resolved
<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
robbie-c marked this conversation as resolved.
Show resolved Hide resolved
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
robbie-c marked this conversation as resolved.
Show resolved Hide resolved

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
robbie-c marked this conversation as resolved.
Show resolved Hide resolved
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
Loading