Skip to content

Commit

Permalink
feat(web-analytics): LCP Score in web analytics (#25252)
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
robbie-c and github-actions[bot] authored Oct 1, 2024
1 parent c31db01 commit 11401a9
Show file tree
Hide file tree
Showing 14 changed files with 219 additions and 18 deletions.
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export const FEATURE_FLAGS = {
DATA_MODELING: 'data-modeling', // owner: @EDsCODE #team-data-warehouse
WEB_ANALYTICS_CONVERSION_GOALS: 'web-analytics-conversion-goals', // owner: @robbie-c
WEB_ANALYTICS_LAST_CLICK: 'web-analytics-last-click', // owner: @robbie-c
WEB_ANALYTICS_LCP_SCORE: 'web-analytics-lcp-score', // owner: @robbie-c
HEDGEHOG_SKIN_SPIDERHOG: 'hedgehog-skin-spiderhog', // owner: @benjackwhite
INSIGHT_VARIABLES: 'insight_variables', // owner: @Gilbert09 #team-data-warehouse
WEB_EXPERIMENTS: 'web-experiments', // owner: @team-feature-success
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/lib/taxonomy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,16 @@ export const CORE_FILTER_DEFINITIONS_BY_GROUP = {
description: <span>The last external URL clicked in this session</span>,
examples: ['https://example.com/interesting-article?parameter=true'],
},
$vitals_lcp: {
label: 'Web vitals LCP',
description: (
<span>
The time it took for the Largest Contentful Paint on the page. This captures the perceived load time
of the page, and measure how long it took for the main content of the page to be visible to users.
</span>
),
examples: ['2.2'],
},
},
groups: {
$group_key: {
Expand Down
21 changes: 19 additions & 2 deletions frontend/src/queries/nodes/WebOverview/WebOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { IconTrending } from '@posthog/icons'
import { LemonSkeleton } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { getColorVar } from 'lib/colors'
import { FEATURE_FLAGS } from 'lib/constants'
import { IconTrendingDown, IconTrendingFlat } from 'lib/lemon-ui/icons'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { humanFriendlyDuration, humanFriendlyLargeNumber, isNotNil, range } from 'lib/utils'
import { useState } from 'react'

Expand Down Expand Up @@ -34,13 +36,14 @@ export function WebOverview(props: {
onData,
dataNodeCollectionId: dataNodeCollectionId ?? key,
})
const { featureFlags } = useValues(featureFlagLogic)
const { response, responseLoading } = useValues(logic)

const webOverviewQueryResponse = response as WebOverviewQueryResponse | undefined

const samplingRate = webOverviewQueryResponse?.samplingRate

const numSkeletons = props.query.conversionGoal ? 4 : 5
const numSkeletons = props.query.conversionGoal ? 4 : featureFlags[FEATURE_FLAGS.WEB_ANALYTICS_LCP_SCORE] ? 6 : 5

return (
<>
Expand Down Expand Up @@ -136,7 +139,19 @@ const formatPercentage = (x: number, options?: { precise?: boolean }): string =>
return (x / 100).toLocaleString(undefined, { style: 'percent', maximumFractionDigits: 0 })
}

const formatSeconds = (x: number): string => humanFriendlyDuration(Math.round(x))
const formatSeconds = (x: number): string => {
// if over than a minute, show minutes and seconds
if (x >= 60) {
return humanFriendlyDuration(x)
}
// if over 1 second, show 3 significant figures
if (x >= 1) {
return `${x.toPrecision(3)}s`
}

// show the number of milliseconds
return `${x * 1000}ms`
}

const formatUnit = (x: number, options?: { precise?: boolean }): string => {
if (options?.precise) {
Expand Down Expand Up @@ -172,6 +187,8 @@ const labelFromKey = (key: string): string => {
return 'Session duration'
case 'bounce rate':
return 'Bounce rate'
case 'lcp score':
return 'LCP Score'
case 'conversion rate':
return 'Conversion rate'
case 'total conversions':
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11181,6 +11181,9 @@
"filterTestAccounts": {
"type": "boolean"
},
"includeLCPScore": {
"type": "boolean"
},
"kind": {
"const": "WebOverviewQuery",
"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 @@ -1395,6 +1395,7 @@ interface WebAnalyticsQueryBase<R extends Record<string, any>> extends DataNode<
export interface WebOverviewQuery extends WebAnalyticsQueryBase<WebOverviewQueryResponse> {
kind: NodeKind.WebOverviewQuery
compare?: boolean
includeLCPScore?: boolean
}

export interface WebOverviewItem {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,7 @@ export const webAnalyticsLogic = kea<webAnalyticsLogicType>([
compare,
filterTestAccounts,
conversionGoal,
includeLCPScore: featureFlags[FEATURE_FLAGS.WEB_ANALYTICS_LCP_SCORE] ? true : undefined,
},
insightProps: createInsightProps(TileId.OVERVIEW),
canOpenModal: false,
Expand Down
1 change: 1 addition & 0 deletions posthog/api/test/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def test_expected_session_properties(self):
"$start_timestamp",
"$is_bounce",
"$last_external_click_url",
"$vitals_lcp",
}
assert actual_properties == expected_properties

Expand Down
3 changes: 3 additions & 0 deletions posthog/demo/products/hedgebox/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,3 +882,6 @@ def set_project_up(self, team, user):
)
except IntegrityError:
pass # This can happen if demo data generation is re-run for the same project

# autocapture
team.autocapture_web_vitals_opt_in = True
5 changes: 5 additions & 0 deletions posthog/hogql/database/schema/sessions_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"screen_uniq": DatabaseField(name="screen_uniq"),
"last_external_click_url": StringDatabaseField(name="last_external_click_url"),
"page_screen_autocapture_uniq_up_to": DatabaseField(name="page_screen_autocapture_uniq_up_to"),
"vitals_lcp": DatabaseField(name="vitals_lcp"),
}

LAZY_SESSIONS_FIELDS: dict[str, FieldOrTable] = {
Expand Down Expand Up @@ -96,6 +97,7 @@
# some aliases for people upgrading from v1 to v2
"$exit_current_url": StringDatabaseField(name="$exit_current_url"),
"$exit_pathname": StringDatabaseField(name="$exit_pathname"),
"$vitals_lcp": FloatDatabaseField(name="vitals_lcp", nullable=True),
}


Expand Down Expand Up @@ -128,6 +130,7 @@ def avoid_asterisk_fields(self) -> list[str]:
"screen_uniq",
"last_external_click_url",
"page_screen_autocapture_uniq_up_to",
"vitals_lcp",
]


Expand Down Expand Up @@ -216,6 +219,7 @@ def arg_max_merge_field(field_name: str) -> ast.Call:
params=[ast.Constant(value=1)],
args=[ast.Field(chain=[table_name, "page_screen_autocapture_uniq_up_to"])],
),
"$vitals_lcp": ast.Call(name="argMinMerge", args=[ast.Field(chain=[table_name, "vitals_lcp"])]),
}
# Alias
aggregate_fields["id"] = aggregate_fields["session_id"]
Expand Down Expand Up @@ -476,6 +480,7 @@ def is_match(field_name: str) -> bool:
"$end_current_url": "finalizeAggregation(end_url)",
"$end_pathname": "path(finalizeAggregation(end_url))",
"$last_external_click_url": "finalizeAggregation(last_external_click_url)",
"$vitals_lcp": "finalizeAggregation(vitals_lcp)",
}


Expand Down
48 changes: 48 additions & 0 deletions posthog/hogql/database/schema/test/test_sessions_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,53 @@ def test_last_external_click_url(self):
[row1] = response.results or []
self.assertEqual(row1, ("https://example.com/2",))

def test_lcp(self):
s1 = str(uuid7("2024-10-01"))
s2 = str(uuid7("2024-10-02"))
s3 = str(uuid7("2024-10-03"))
s4 = str(uuid7("2024-10-04"))

# should be null if there's no $web_vitals_LCP_value
_create_event(
event="$pageview",
team=self.team,
distinct_id=s1,
properties={"$session_id": s1, "$pathname": "/1"},
)
# should be able to read the property off the regular "$web_vitals" event
_create_event(
event="$web_vitals",
team=self.team,
distinct_id=s2,
properties={"$session_id": s2, "$web_vitals_LCP_value": "2.0"},
)
# should be able to read the property off any event
_create_event(
event="$pageview",
team=self.team,
distinct_id=s3,
properties={"$session_id": s3, "$web_vitals_LCP_value": "3.0"},
)
# should take the first value if there's multiple
_create_event(
event="$web_vitals",
team=self.team,
distinct_id=s4,
properties={"$session_id": s4, "$web_vitals_LCP_value": "4.1"},
)
_create_event(
event="$web_vitals",
team=self.team,
distinct_id=s4,
properties={"$session_id": s4, "$web_vitals_LCP_value": "4.2"},
)
response = self.__execute(
parse_select("select $vitals_lcp from sessions order by session_id"),
)

rows = response.results or []
assert rows == [(None,), (2.0,), (3.0,), (4.1,)]

def test_can_use_v1_and_v2_fields(self):
session_id = str(uuid7())

Expand Down Expand Up @@ -604,6 +651,7 @@ def test_all(self):
"$screen_count",
"$session_duration",
"$start_timestamp",
"$vitals_lcp",
},
)
self.assertEqual(
Expand Down
32 changes: 28 additions & 4 deletions posthog/hogql/database/test/__snapshots__/test_database.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@
"$last_external_click_url",
"$page_screen_autocapture_count_up_to",
"$exit_current_url",
"$exit_pathname"
"$exit_pathname",
"$vitals_lcp"
],
"hogql_value": "session",
"id": "session",
Expand Down Expand Up @@ -962,7 +963,8 @@
"$last_external_click_url",
"$page_screen_autocapture_count_up_to",
"$exit_current_url",
"$exit_pathname"
"$exit_pathname",
"$vitals_lcp"
],
"hogql_value": "session",
"id": "session",
Expand Down Expand Up @@ -1456,6 +1458,16 @@
"schema_valid": true,
"table": null,
"type": "string"
},
"$vitals_lcp": {
"chain": null,
"fields": null,
"hogql_value": "`$vitals_lcp`",
"id": null,
"name": "$vitals_lcp",
"schema_valid": true,
"table": null,
"type": "float"
}
},
"id": "sessions",
Expand Down Expand Up @@ -1967,7 +1979,8 @@
"$last_external_click_url",
"$page_screen_autocapture_count_up_to",
"$exit_current_url",
"$exit_pathname"
"$exit_pathname",
"$vitals_lcp"
],
"hogql_value": "session",
"id": "session",
Expand Down Expand Up @@ -2498,7 +2511,8 @@
"$last_external_click_url",
"$page_screen_autocapture_count_up_to",
"$exit_current_url",
"$exit_pathname"
"$exit_pathname",
"$vitals_lcp"
],
"hogql_value": "session",
"id": "session",
Expand Down Expand Up @@ -2992,6 +3006,16 @@
"schema_valid": true,
"table": null,
"type": "string"
},
"$vitals_lcp": {
"chain": null,
"fields": null,
"hogql_value": "`$vitals_lcp`",
"id": null,
"name": "$vitals_lcp",
"schema_valid": true,
"table": null,
"type": "float"
}
},
"id": "sessions",
Expand Down
Loading

0 comments on commit 11401a9

Please sign in to comment.