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(cdp): App metrics #23893

Merged
merged 38 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
fac3a13
Added log metrics all over the place
benjackwhite Jul 22, 2024
7b9ec81
Fix up metrics
benjackwhite Jul 22, 2024
4ba12fa
Fixes
benjackwhite Jul 22, 2024
bd965a4
Fixes
benjackwhite Jul 22, 2024
bd093c5
Fix up label generation
benjackwhite Jul 23, 2024
246fba4
Fixes
benjackwhite Jul 23, 2024
0695755
Added to spark lines
benjackwhite Jul 23, 2024
4c4a54e
Fixes
benjackwhite Jul 23, 2024
bc59991
Added tooltipFixes
benjackwhite Jul 23, 2024
e677250
Fixes
benjackwhite Jul 23, 2024
d66fe51
Merge branch 'master' into feat/cdp-app-metrics
benjackwhite Jul 23, 2024
2dae006
Fix
benjackwhite Jul 23, 2024
38d67c9
Fix tests
benjackwhite Jul 23, 2024
79e559b
fix
benjackwhite Jul 23, 2024
6482fc4
Fix urls
benjackwhite Jul 23, 2024
8043dd6
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
1160d58
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
b944f78
Merge branch 'master' of github.com:PostHog/posthog into feat/cdp-app…
benjackwhite Jul 23, 2024
0eab788
Merge branches 'feat/cdp-app-metrics' and 'feat/cdp-app-metrics' of g…
benjackwhite Jul 23, 2024
91a8f56
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
6027253
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
a2cb741
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
b8d10a8
Merge branch 'master' into feat/cdp-app-metrics
benjackwhite Jul 23, 2024
a5c2817
Removed instance_id
benjackwhite Jul 23, 2024
28ca47b
Fix
benjackwhite Jul 23, 2024
75de822
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
26bc69e
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
c5ca1f8
Update UI snapshots for `chromium` (2)
github-actions[bot] Jul 23, 2024
243e65f
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
97eb657
Update UI snapshots for `chromium` (2)
github-actions[bot] Jul 23, 2024
9876bdd
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
ce4dd21
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
2962e1e
Update UI snapshots for `chromium` (2)
github-actions[bot] Jul 23, 2024
72b35d3
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
221a335
Update UI snapshots for `chromium` (2)
github-actions[bot] Jul 23, 2024
62202a5
Merge branch 'master' into feat/cdp-app-metrics
benjackwhite Jul 23, 2024
1eda3de
Merge branch 'feat/cdp-app-metrics' of github.com:PostHog/posthog int…
benjackwhite Jul 23, 2024
8c8f25b
Update UI snapshots for `chromium` (1)
github-actions[bot] Jul 23, 2024
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
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.
15 changes: 15 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
ActionType,
ActivityScope,
AlertType,
AppMetricsTotalsV2Response,
AppMetricsV2RequestParams,
AppMetricsV2Response,
BatchExportConfiguration,
BatchExportRun,
CohortType,
Expand Down Expand Up @@ -1626,6 +1629,18 @@ const api = {
): Promise<PaginatedResponse<LogEntry>> {
return await new ApiRequest().hogFunction(id).withAction('logs').withQueryString(params).get()
},
async metrics(
id: HogFunctionType['id'],
params: AppMetricsV2RequestParams = {}
): Promise<AppMetricsV2Response> {
return await new ApiRequest().hogFunction(id).withAction('metrics').withQueryString(params).get()
},
async metricsTotals(
id: HogFunctionType['id'],
params: Partial<AppMetricsV2RequestParams> = {}
): Promise<AppMetricsTotalsV2Response> {
return await new ApiRequest().hogFunction(id).withAction('metrics/totals').withQueryString(params).get()
},

async listTemplates(): Promise<PaginatedResponse<HogFunctionTemplateType>> {
return await new ApiRequest().hogFunctionTemplates().get()
Expand Down
29 changes: 28 additions & 1 deletion frontend/src/scenes/pipeline/AppMetricSparkLine.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useValues } from 'kea'
import { useActions, useValues } from 'kea'
import { Sparkline, SparklineTimeSeries } from 'lib/components/Sparkline'
import { useEffect } from 'react'

import { pipelineNodeMetricsLogic } from './pipelineNodeMetricsLogic'
import { pipelineNodeMetricsV2Logic } from './pipelineNodeMetricsV2Logic'
import { PipelineBackend, PipelineNode } from './types'

export function AppMetricSparkLine({ pipelineNode }: { pipelineNode: PipelineNode }): JSX.Element {
Expand Down Expand Up @@ -34,3 +36,28 @@ export function AppMetricSparkLine({ pipelineNode }: { pipelineNode: PipelineNod
}
return <Sparkline loading={appMetricsResponse === null} labels={dates} data={displayData} className="max-w-24" />
}

export function AppMetricSparkLineV2({ pipelineNode }: { pipelineNode: PipelineNode }): JSX.Element {
const logic = pipelineNodeMetricsV2Logic({ id: `${pipelineNode.id}`.replace('hog-', '') })
const { appMetrics, appMetricsLoading } = useValues(logic)
const { loadMetrics } = useActions(logic)

useEffect(() => {
loadMetrics()
}, [])

const displayData: SparklineTimeSeries[] = [
{
color: 'success',
name: 'Success',
values: appMetrics?.series.find((s) => s.name === 'succeeded')?.values || [],
},
{
color: 'danger',
name: 'Failures',
values: appMetrics?.series.find((s) => s.name === 'failed')?.values || [],
},
]

return <Sparkline loading={appMetricsLoading} labels={appMetrics?.labels} data={displayData} className="max-w-24" />
}
7 changes: 3 additions & 4 deletions frontend/src/scenes/pipeline/PipelineNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { BatchExportRuns } from './BatchExportRuns'
import { PipelineNodeConfiguration } from './PipelineNodeConfiguration'
import { pipelineNodeLogic, PipelineNodeLogicProps } from './pipelineNodeLogic'
import { PipelineNodeMetrics } from './PipelineNodeMetrics'
import { PipelineNodeMetricsV2 } from './PipelineNodeMetricsV2'
import { PipelineBackend } from './types'

export const PIPELINE_TAB_TO_NODE_STAGE: Partial<Record<PipelineTab, PipelineStage>> = {
Expand Down Expand Up @@ -58,10 +59,8 @@ export function PipelineNode(params: { stage?: string; id?: string } = {}): JSX.
[PipelineNodeTab.Configuration]: <PipelineNodeConfiguration />,
}

if ([PipelineBackend.Plugin, PipelineBackend.BatchExport].includes(node.backend)) {
tabToContent[PipelineNodeTab.Metrics] = <PipelineNodeMetrics id={id} />
}

tabToContent[PipelineNodeTab.Metrics] =
node.backend === PipelineBackend.HogFunction ? <PipelineNodeMetricsV2 /> : <PipelineNodeMetrics id={id} />
tabToContent[PipelineNodeTab.Logs] = <PipelineNodeLogs id={id} stage={stage} />

if (node.backend === PipelineBackend.BatchExport) {
Expand Down
251 changes: 251 additions & 0 deletions frontend/src/scenes/pipeline/PipelineNodeMetricsV2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { IconCalendar } from '@posthog/icons'
import { LemonSelect, LemonSkeleton, Popover, SpinnerOverlay, Tooltip } from '@posthog/lemon-ui'
import { BindLogic, useActions, useValues } from 'kea'
import { Chart, ChartDataset, ChartItem } from 'lib/Chart'
import { getColorVar } from 'lib/colors'
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
import { humanFriendlyNumber, inStorybookTestRunner } from 'lib/utils'
import { useEffect, useRef, useState } from 'react'
import { InsightTooltip } from 'scenes/insights/InsightTooltip/InsightTooltip'

import { pipelineNodeLogic } from './pipelineNodeLogic'
import { pipelineNodeMetricsV2Logic } from './pipelineNodeMetricsV2Logic'
import { PipelineBackend } from './types'

const METRICS_INFO = {
succeeded: 'Total number of events processed successfully',
failed: 'Total number of events that had errors during processing',
filtered: 'Total number of events that were filtered out',
disabled_temporarily:
'Total number of events that were skipped due to the destination being temporarily disabled (due to issues such as the destination being down or rate-limited)',
disabled_permanently:
'Total number of events that were skipped due to the destination being permanently disabled (due to prolonged issues with the destination)',
}

export function PipelineNodeMetricsV2(): JSX.Element {
const { node } = useValues(pipelineNodeLogic)

const logic = pipelineNodeMetricsV2Logic({ id: `${node.id}` })

const { filters } = useValues(logic)
const { setFilters, loadMetrics, loadMetricsTotals } = useActions(logic)

useEffect(() => {
loadMetrics()
loadMetricsTotals()
}, [])

if (node.backend !== PipelineBackend.HogFunction) {
return <div>Metrics not available for this node</div>
}

return (
<BindLogic logic={pipelineNodeMetricsV2Logic} props={{ id: node.id }}>
<div className="space-y-4">
<AppMetricsTotals />

<div className="flex items-center gap-2">
<h2 className="mb-0">Delivery trends</h2>
<div className="flex-1" />
<LemonSelect
options={[
{ label: 'Hourly', value: 'hour' },
{ label: 'Daily', value: 'day' },
{ label: 'Weekly', value: 'week' },
]}
size="small"
value={filters.interval}
onChange={(value) => setFilters({ interval: value })}
/>
<DateFilter
dateTo={filters.before}
dateFrom={filters.after}
onChange={(from, to) => setFilters({ after: from || undefined, before: to || undefined })}
allowedRollingDateOptions={['days', 'weeks', 'months', 'years']}
makeLabel={(key) => (
<>
<IconCalendar /> {key}
</>
)}
/>
</div>

<AppMetricsGraph />
</div>
</BindLogic>
)
}

function AppMetricBigNumber({
label,
value,
tooltip,
}: {
label: string
value: number | undefined
tooltip: JSX.Element | string
}): JSX.Element {
return (
<Tooltip title={tooltip}>
<div className="border p-2 rounded bg-bg-light flex-1 flex flex-col gap-2 items-center">
<div className="uppercase font-bold text-xs">{label.replace(/_/g, ' ')}</div>
<div className="text-2xl flex-1 mb-2 flex items-center">{humanFriendlyNumber(value ?? 0)}</div>
</div>
</Tooltip>
)
}

function AppMetricsTotals(): JSX.Element {
const { appMetricsTotals, appMetricsTotalsLoading } = useValues(pipelineNodeMetricsV2Logic)

return (
<div className="space-y-4">
<div className="flex items-center gap-2 flex-wrap">
{Object.entries(METRICS_INFO).map(([key, value]) => (
<div key={key} className="flex flex-col h-30 min-w-30 flex-1 max-w-100">
{appMetricsTotalsLoading ? (
<LemonSkeleton className="h-full w-full" />
) : (
<AppMetricBigNumber label={key} value={appMetricsTotals?.totals?.[key]} tooltip={value} />
)}
</div>
))}
</div>
</div>
)
}

function AppMetricsGraph(): JSX.Element {
const { appMetrics, appMetricsLoading } = useValues(pipelineNodeMetricsV2Logic)
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const [popoverContent, setPopoverContent] = useState<JSX.Element | null>(null)
const [tooltipState, setTooltipState] = useState({ x: 0, y: 0, visible: false })

useEffect(() => {
let chart: Chart
if (canvasRef.current && appMetrics && !inStorybookTestRunner()) {
chart = new Chart(canvasRef.current?.getContext('2d') as ChartItem, {
type: 'line',
data: {
labels: appMetrics.labels,
datasets: [
...appMetrics.series.map((series) => ({
label: series.name,
data: series.values,
borderColor: '',
...colorConfig(series.name),
})),
],
},
options: {
scales: {
x: {
ticks: {
maxRotation: 0,
},
grid: {
display: false,
},
},
y: {
beginAtZero: true,
},
},
plugins: {
// @ts-expect-error Types of library are out of date
crosshair: false,
legend: {
display: false,
},
tooltip: {
enabled: false, // Using external tooltip
external({ tooltip, chart }) {
setPopoverContent(
<InsightTooltip
embedded
hideInspectActorsSection
// showHeader={!!labels}
altTitle={tooltip.dataPoints[0].label}
seriesData={tooltip.dataPoints.map((dp, i) => ({
id: i,
dataIndex: 0,
datasetIndex: 0,
label: dp.dataset.label,
color: dp.dataset.borderColor as string,
count: (dp.dataset.data?.[dp.dataIndex] as number) || 0,
}))}
renderSeries={(value) => value}
renderCount={(count) => humanFriendlyNumber(count)}
/>
)

const position = chart.canvas.getBoundingClientRect()
setTooltipState({
x: position.left + tooltip.caretX,
y: position.top + tooltip.caretY,
visible: tooltip.opacity > 0,
})
},
},
},
maintainAspectRatio: false,
interaction: {
mode: 'index',
axis: 'x',
intersect: false,
},
},
})

return () => {
chart?.destroy()
}
}
}, [appMetrics])

return (
<div className="relative border rounded p-6 bg-bg-light h-[50vh]">
{appMetricsLoading && <SpinnerOverlay />}
{!!appMetrics && <canvas ref={canvasRef} />}
<Popover
visible={tooltipState.visible}
overlay={popoverContent}
placement="top"
padded={false}
className="pointer-events-none"
>
<div
className="fixed"
// eslint-disable-next-line react/forbid-dom-props
style={{ left: tooltipState.x, top: tooltipState.y }}
/>
</Popover>
</div>
)
}

function colorConfig(name: string): Partial<ChartDataset<'line', any>> {
let color = ''

switch (name) {
case 'succeeded':
color = getColorVar('success')
break
case 'failed':
color = getColorVar('danger')
break
default:
color = getColorVar('data-color-1')
break
}

return {
borderColor: color,
hoverBorderColor: color,
hoverBackgroundColor: color,
backgroundColor: color,
fill: false,
borderWidth: 2,
pointRadius: 0,
}
}
8 changes: 6 additions & 2 deletions frontend/src/scenes/pipeline/destinations/Destinations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { urls } from 'scenes/urls'

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

import { AppMetricSparkLine } from '../AppMetricSparkLine'
import { AppMetricSparkLine, AppMetricSparkLineV2 } from '../AppMetricSparkLine'
import { HogFunctionIcon } from '../hogfunctions/HogFunctionIcon'
import { NewButton } from '../NewButton'
import { pipelineAccessLogic } from '../pipelineAccessLogic'
Expand Down Expand Up @@ -161,7 +161,11 @@ export function DestinationsTable(props: PipelineDestinationsLogicProps): JSX.El
PipelineNodeTab.Metrics
)}
>
<AppMetricSparkLine pipelineNode={destination} />
{destination.backend === PipelineBackend.HogFunction ? (
<AppMetricSparkLineV2 pipelineNode={destination} />
) : (
<AppMetricSparkLine pipelineNode={destination} />
)}
</Link>
)
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/scenes/pipeline/pipelineNodeLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ export const pipelineNodeLogic = kea<pipelineNodeLogicType>([
setCurrentTab: () => [urls.pipelineNode(props.stage as PipelineStage, props.id, values.currentTab)],
}
}),
urlToAction(({ actions, values }) => ({
'/pipeline/:stage/:id/:nodeTab': ({ nodeTab }) => {
urlToAction(({ props, actions, values }) => ({
[urls.pipelineNode(props.stage as PipelineStage, props.id, ':nodeTab')]: ({ nodeTab }) => {
if (nodeTab !== values.currentTab && Object.values(PipelineNodeTab).includes(nodeTab as PipelineNodeTab)) {
actions.setCurrentTab(nodeTab as PipelineNodeTab)
}
Expand Down
Loading
Loading