Skip to content

Commit

Permalink
Merge branch 'master' of github.com:PostHog/posthog into chore/produc…
Browse files Browse the repository at this point in the history
…ts-folder-scripts
  • Loading branch information
skoob13 committed Jan 21, 2025
2 parents 69dba8b + b3f06db commit 90e8d80
Show file tree
Hide file tree
Showing 70 changed files with 6,330 additions and 609 deletions.
58 changes: 58 additions & 0 deletions ee/clickhouse/models/test/test_cohort.py
Original file line number Diff line number Diff line change
Expand Up @@ -1447,3 +1447,61 @@ def test_calculate_people_ch_in_multiteam_project(self):

self.assertCountEqual([r[0] for r in results_team1], [person2_team1.uuid])
self.assertCountEqual([r[0] for r in results_team2], [person1_team2.uuid])

def test_cohortpeople_action_all_events(self):
# Create an action that matches all events (no specific event defined)
action = Action.objects.create(team=self.team, name="all events", steps_json=[{"event": None}])

# Create two people
Person.objects.create(
team_id=self.team.pk,
distinct_ids=["1"],
properties={"$some_prop": "something", "$another_prop": "something"},
)

Person.objects.create(
team_id=self.team.pk,
distinct_ids=["2"],
properties={"$some_prop": "something", "$another_prop": "something"},
)

# Create different types of events for both people
_create_event(
event="$pageview",
team=self.team,
distinct_id="1",
properties={"attr": "some_val"},
timestamp=datetime.now() - timedelta(hours=12),
)

_create_event(
event="$autocapture",
team=self.team,
distinct_id="2",
properties={"attr": "some_val"},
timestamp=datetime.now() - timedelta(hours=12),
)

# Create a cohort based on the "all events" action
cohort = Cohort.objects.create(
team=self.team, groups=[{"action_id": action.pk, "days": 1}], name="cohort_all_events"
)
cohort.calculate_people_ch(pending_version=0)

# Both people should be in the cohort since they both performed some event
results = self._get_cohortpeople(cohort)
self.assertEqual(len(results), 2)

# Create a person with no events
Person.objects.create(
team_id=self.team.pk,
distinct_ids=["3"],
properties={"$some_prop": "something", "$another_prop": "something"},
)

# Recalculate cohort
cohort.calculate_people_ch(pending_version=1)

# Should still only have 2 people since person 3 has no events
results = self._get_cohortpeople(cohort)
self.assertEqual(len(results), 2)
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
e."$group_0" as aggregation_target,
if(notEmpty(pdi.distinct_id), pdi.person_id, e.person_id) as person_id,
person.person_props as person_props,
person.pmat_email as pmat_email,
if(event = 'step one', 1, 0) as step_0,
if(step_0 = 1, timestamp, null) as latest_0,
if(event = 'step two', 1, 0) as step_1,
Expand All @@ -80,7 +79,6 @@
HAVING argMax(is_deleted, version) = 0) AS pdi ON e.distinct_id = pdi.distinct_id
INNER JOIN
(SELECT id,
argMax(pmat_email, version) as pmat_email,
argMax(properties, version) as person_props
FROM person
WHERE team_id = 99999
Expand All @@ -97,7 +95,7 @@
AND event IN ['step one', 'step three', 'step two']
AND toTimeZone(timestamp, 'UTC') >= toDateTime('2021-05-01 00:00:00', 'UTC')
AND toTimeZone(timestamp, 'UTC') <= toDateTime('2021-05-10 23:59:59', 'UTC')
AND (("pmat_email" ILIKE '%g0%'
AND ((replaceRegexpAll(JSONExtractRaw(person_props, 'email'), '^"|"$', '') ILIKE '%g0%'
OR replaceRegexpAll(JSONExtractRaw(person_props, 'name'), '^"|"$', '') ILIKE '%g0%'
OR replaceRegexpAll(JSONExtractRaw(e.properties, 'distinct_id'), '^"|"$', '') ILIKE '%g0%'
OR replaceRegexpAll(JSONExtractRaw(group_properties_0, 'name'), '^"|"$', '') ILIKE '%g0%'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { IconOpenSidebar, IconPlus } from '@posthog/icons'
import { LemonButton } from '@posthog/lemon-ui'
import { useActions } from 'kea'
import { router } from 'kea-router'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { ProductCrossSellLocation, trackProductCrossSell } from 'lib/utils/cross-sell'
import type React from 'react'
import { urls } from 'scenes/urls'

import { PipelineStage } from '~/types'
import { PipelineStage, ProductKey } from '~/types'

import { BuilderHog3 } from '../hedgehogs'

Expand All @@ -21,6 +24,8 @@ type EmptyStateProps = {
}

const EmptyState = ({ title, description, action, docsUrl, hog: Hog, groupType }: EmptyStateProps): JSX.Element => {
const { push } = useActions(router)

return (
<div className="w-full p-8 rounded mt-4 flex items-center gap-4">
<div className="w-32 h-32">
Expand All @@ -33,7 +38,16 @@ const EmptyState = ({ title, description, action, docsUrl, hog: Hog, groupType }
<LemonButton
type="primary"
icon={<IconPlus />}
to={action.to}
onClick={() => {
trackProductCrossSell({
from: ProductKey.PRODUCT_ANALYTICS,
to: ProductKey.DATA_WAREHOUSE,
location: ProductCrossSellLocation.TAXONOMIC_FILTER_EMPTY_STATE,
context: {},
})

push(action.to)
}}
data-attr={`taxonomic-filter-empty-state-${groupType}-new-button`}
>
{action.text}
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/lib/utils/cross-sell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import posthog from 'posthog-js'

import type { ProductKey } from '~/types'

export type ProductCrossSellContext = Record<string, unknown>

export enum ProductCrossSellLocation {
TAXONOMIC_FILTER_EMPTY_STATE = 'taxonomic_filter_empty_state',
}

export type ProductCrossSellProperties = {
from: ProductKey
to: ProductKey
location: ProductCrossSellLocation
context?: ProductCrossSellContext
}

export function trackProductCrossSell(properties: ProductCrossSellProperties): void {
posthog.capture('product cross sell interaction', properties)
}
2 changes: 1 addition & 1 deletion frontend/src/queries/nodes/WebVitals/WebVitals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function WebVitals(props: {
/>
</div>

<div className="flex flex-row gap-2 p-4">
<div className="flex flex-col sm:flex-row gap-2 p-4">
<WebVitalsContent webVitalsQueryResponse={webVitalsQueryResponse} />
<div className="flex-1">
<Query query={webVitalsMetricQuery} readOnly embedded />
Expand Down
24 changes: 8 additions & 16 deletions frontend/src/queries/nodes/WebVitals/WebVitalsPathBreakdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { QueryContext } from '~/queries/types'

import { dataNodeLogic } from '../DataNode/dataNodeLogic'
import { getValueWithUnit, ICON_PER_BAND } from './definitions'
import { computePositionInBand, getValueWithUnit, ICON_PER_BAND } from './definitions'

let uniqueNode = 0
export function WebVitalsPathBreakdown(props: {
Expand Down Expand Up @@ -147,7 +147,7 @@ const Content = ({
<LemonSkeleton fade className={clsx('w-full', SKELETON_HEIGHT[band])} />
) : values?.length ? (
values?.map(({ path, value }) => {
const width = computeWidth(value, threshold)
const width = computePositionInBand(value, threshold) * 100

return (
<div
Expand All @@ -159,8 +159,12 @@ const Content = ({
// eslint-disable-next-line react/forbid-dom-props
style={{ width, backgroundColor: 'var(--neutral-250)', opacity: 0.5 }}
/>
<span className="relative z-10">{path}</span>
<span className="relative z-10">{value}</span>
<span title={path} className="relative z-10 truncate mr-2 flex-1">
{path}
</span>
<span className="relative z-10 flex-shrink-0">
{value >= 1 ? value.toFixed(0) : value.toFixed(2)}
</span>
</div>
)
})
Expand All @@ -174,15 +178,3 @@ const Content = ({
</div>
)
}

const computeWidth = (value: number, threshold: { good: number; poor: number }): string => {
if (value < threshold.good) {
return `${(value / threshold.good) * 100}%`
}

if (value > threshold.poor) {
return `${((value - threshold.poor) / (threshold.good - threshold.poor)) * 100}%`
}

return `${((value - threshold.good) / (threshold.poor - threshold.good)) * 100}%`
}
59 changes: 40 additions & 19 deletions frontend/src/queries/nodes/WebVitals/WebVitalsProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import clsx from 'clsx'
import { WebVitalsThreshold } from 'scenes/web-analytics/webAnalyticsLogic'

import { getMetricBand, getThresholdColor } from './definitions'
import { WebVitalsMetricBand } from '~/queries/schema'

import { computePositionInBand, getMetricBand, getThresholdColor } from './definitions'

interface WebVitalsProgressBarProps {
value?: number
threshold: WebVitalsThreshold
}

export function WebVitalsProgressBar({ value, threshold }: WebVitalsProgressBarProps): JSX.Element {
const indicatorPercentage = Math.min((value ?? 0 / threshold.end) * 100, 100)

const thresholdColor = getThresholdColor(value, threshold)
const band = getMetricBand(value, threshold)

const goodWidth = (threshold.good / threshold.end) * 100
Expand All @@ -25,7 +24,9 @@ export function WebVitalsProgressBar({ value, threshold }: WebVitalsProgressBarP
className={clsx('absolute h-full rounded-full', band === 'good' ? 'bg-success' : 'bg-muted')}
// eslint-disable-next-line react/forbid-dom-props
style={{ width: `${goodWidth}%` }}
/>
>
<IndicatorLine value={value} threshold={threshold} band="good" />
</div>

{/* Yellow segment up to "poor" threshold */}
<div
Expand All @@ -35,26 +36,46 @@ export function WebVitalsProgressBar({ value, threshold }: WebVitalsProgressBarP
)}
// eslint-disable-next-line react/forbid-dom-props
style={{ left: `${goodWidth + 1}%`, width: `${improvementsWidth - 1}%` }}
/>
>
<IndicatorLine value={value} threshold={threshold} band="needs_improvements" />
</div>

{/* Red segment after "poor" threshold */}
<div
className={clsx('absolute h-full rounded-full', band === 'poor' ? 'bg-danger' : 'bg-muted')}
// eslint-disable-next-line react/forbid-dom-props
style={{ left: `${goodWidth + improvementsWidth + 1}%`, width: `${poorWidth - 1}%` }}
/>

{/* Indicator line */}
{value != null && (
<div
className={clsx('absolute w-0.5 h-3 -top-1', `bg-${thresholdColor}`)}
// eslint-disable-next-line react/forbid-dom-props
style={{
left: `${indicatorPercentage}%`,
transform: 'translateX(-50%)',
}}
/>
)}
>
<IndicatorLine value={value} threshold={threshold} band="poor" />
</div>
</div>
)
}

type IndicatorLineProps = {
value: number | undefined
threshold: WebVitalsThreshold
band: WebVitalsMetricBand | 'none'
}

const IndicatorLine = ({ value, threshold, band }: IndicatorLineProps): JSX.Element | null => {
if (!value) {
return null
}

const thisBand = getMetricBand(value, threshold)
if (thisBand !== band) {
return null
}

const positionInBand = computePositionInBand(value, threshold)
const color = getThresholdColor(value, threshold)

return (
<div
// eslint-disable-next-line react/forbid-dom-props
style={{ left: `${positionInBand * 100}%` }}
className={clsx('absolute w-0.5 h-3 -top-1', `bg-${color}`)}
/>
)
}
17 changes: 17 additions & 0 deletions frontend/src/queries/nodes/WebVitals/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,20 @@ export const getThresholdColor = (value: number | undefined, threshold: WebVital

return 'danger'
}

// Returns a value between 0 and 1 that represents the position of the value inside that band
//
// Useful to display the indicator line in the progress bar
// or the width of the segment in the path breakdown
export const computePositionInBand = (value: number, threshold: WebVitalsThreshold): number => {
if (value <= threshold.good) {
return value / threshold.good
}

// Values can be much higher than what we consider the end, so max out at 1
if (value > threshold.poor) {
return Math.min((value - threshold.poor) / (threshold.end - threshold.poor), 1)
}

return (value - threshold.good) / (threshold.poor - threshold.good)
}
17 changes: 2 additions & 15 deletions frontend/src/queries/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import posthog from 'posthog-js'

import { OnlineExportContext, QueryExportContext } from '~/types'

import { QueryWebSocketManager } from './queryWebSocket'
import {
DashboardFilter,
DataNode,
Expand Down Expand Up @@ -80,8 +79,6 @@ export async function pollForResults(
throw new Error(QUERY_TIMEOUT_ERROR_MESSAGE)
}

let socket: null | QueryWebSocketManager = null

/**
* Execute a query node and return the response, use async query if enabled
*/
Expand All @@ -104,19 +101,9 @@ async function executeQuery<N extends DataNode>(
!SYNC_ONLY_QUERY_KINDS.includes(queryNode.kind) &&
!!featureFlagLogic.findMounted()?.values.featureFlags?.[FEATURE_FLAGS.QUERY_ASYNC]

const refreshParam: RefreshType | undefined =
refresh && isAsyncQuery ? 'force_async' : isAsyncQuery ? 'async' : refresh

if (posthog.isFeatureEnabled('query-websocket')) {
if (!socket) {
socket = new QueryWebSocketManager(
`${window.location.protocol == 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws/query/`
)
}
return socket.sendQuery(queryNode, methodOptions, refreshParam, queryId, filtersOverride, variablesOverride)
}

if (!pollOnly) {
const refreshParam: RefreshType | undefined =
refresh && isAsyncQuery ? 'force_async' : isAsyncQuery ? 'async' : refresh
const response = await api.query(
queryNode,
methodOptions,
Expand Down
Loading

0 comments on commit 90e8d80

Please sign in to comment.