-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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: hackathon metalytics (tracking team usage metrics) #26304
Changes from 45 commits
faf9a05
d631978
a699ede
7b3910a
56dac9d
0e32e8e
ac7867c
0656952
987da60
99629a1
371f082
45b0758
8e4b622
9eff497
16cbc1b
de074f4
ed84770
f90bbe8
89961eb
76736c1
913f846
e2ed316
b866feb
1aea873
07e7d64
bb9a16d
63f2975
3123cbe
67cb581
5d5f4e3
2d2503b
6eb546c
2240d16
fe40166
7685d59
667764c
8ff1c67
3fae703
790691d
8a7fdc3
ec560b3
6c21efd
20e38ba
6e4699b
39e88b3
63c4172
a0badd2
779704f
e87c922
613f039
1b06679
1b6db67
65c2b90
6475d5d
db0021c
3df86e6
4cbae02
f5387e8
50ee5bb
37bd9c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { BindLogic, useValues } from 'kea' | ||
import { metalyticsLogic } from 'lib/components/Metalytics/metalyticsLogic' | ||
import { ProfileBubbles } from 'lib/lemon-ui/ProfilePicture/ProfileBubbles' | ||
import { insightLogic } from 'scenes/insights/insightLogic' | ||
|
||
import { Query } from '~/queries/Query/Query' | ||
import { NodeKind } from '~/queries/schema' | ||
import { hogql } from '~/queries/utils' | ||
|
||
export function SidePanelActivityMetalytics(): JSX.Element { | ||
const { instanceId, viewCount, recentUserMembers } = useValues(metalyticsLogic) | ||
|
||
if (!instanceId) { | ||
return ( | ||
<> | ||
<h3>Metalytics</h3> | ||
<p> | ||
You can see internal analytics of how your Organization members are using PostHog for certain | ||
things. | ||
</p> | ||
</> | ||
) | ||
} | ||
|
||
return ( | ||
<div className="space-y-4 overflow-y-auto max-h-[calc(100vh-200px)]"> | ||
<div className="flex gap-4"> | ||
<div className="flex-1 rounded bg-bg-light p-4"> | ||
<div className="text-muted text-sm">Number Of Sessions</div> | ||
<div className="text-2xl font-semibold">{viewCount ?? 0}</div> | ||
</div> | ||
|
||
<div className="flex-1 rounded bg-bg-light p-4"> | ||
<div className="text-muted text-sm mb-2">Recent Viewers (30 days)</div> | ||
<ProfileBubbles | ||
people={recentUserMembers.map((member) => ({ | ||
email: member.user.email, | ||
name: member.user.first_name, | ||
title: member.user.email, | ||
}))} | ||
tooltip="Recent viewers" | ||
limit={3} | ||
/> | ||
</div> | ||
</div> | ||
|
||
{/* This looks odd but is a weirdness of the Query component it needs to be bound in an insight logic */} | ||
<BindLogic logic={insightLogic} props={{ dashboardItemId: '', doNotLoad: true }}> | ||
<Query | ||
query={{ | ||
display: 'ActionsLineGraph', | ||
chartSettings: { | ||
seriesBreakdownColumn: null, | ||
}, | ||
kind: NodeKind.DataVisualizationNode, | ||
source: { | ||
kind: NodeKind.HogQLQuery, | ||
query: hogql`SELECT timestamp, SUM(count) AS number_of_sessions | ||
FROM app_metrics | ||
WHERE app_source = 'metalytics' | ||
AND instance_id = ${instanceId} | ||
AND timestamp >= NOW() - INTERVAL 30 DAY | ||
GROUP BY timestamp | ||
ORDER BY timestamp DESC`, | ||
}, | ||
}} | ||
/> | ||
</BindLogic> | ||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { IconPulse } from '@posthog/icons' | ||
import { LemonButton } from '@posthog/lemon-ui' | ||
import { useActions, useValues } from 'kea' | ||
import { IconWithCount } from 'lib/lemon-ui/icons' | ||
|
||
import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' | ||
import { SidePanelTab } from '~/types' | ||
|
||
import { metalyticsLogic } from './metalyticsLogic' | ||
|
||
export function MetalyticsSummary(): JSX.Element | null { | ||
const { instanceId, viewCount, viewCountLoading } = useValues(metalyticsLogic) | ||
const safeViewCount = viewCount ?? 0 | ||
const { openSidePanel } = useActions(sidePanelStateLogic) | ||
|
||
if (!instanceId) { | ||
return null | ||
} | ||
|
||
return ( | ||
<IconWithCount count={safeViewCount}> | ||
<LemonButton | ||
loading={viewCountLoading} | ||
icon={<IconPulse />} | ||
size="small" | ||
onClick={() => openSidePanel(SidePanelTab.Activity, 'metalytics')} | ||
tooltip="Click to see more usage data for this tool" | ||
/> | ||
</IconWithCount> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { connect, kea, path, selectors } from 'kea' | ||
import { loaders } from 'kea-loaders' | ||
import { subscriptions } from 'kea-subscriptions' | ||
import api from 'lib/api' | ||
import { membersLogic } from 'scenes/organization/membersLogic' | ||
|
||
import { activityForSceneLogic } from '~/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic' | ||
import { HogQLQuery, NodeKind } from '~/queries/schema' | ||
import { hogql } from '~/queries/utils' | ||
|
||
import type { metalyticsLogicType } from './metalyticsLogicType' | ||
|
||
export const metalyticsLogic = kea<metalyticsLogicType>([ | ||
path(['lib', 'components', 'metalytics', 'metalyticsLogic']), | ||
connect({ | ||
values: [activityForSceneLogic, ['sceneActivityFilters'], membersLogic, ['members']], | ||
}), | ||
|
||
loaders(({ values }) => ({ | ||
viewCount: [ | ||
null as number | null, | ||
{ | ||
loadViewCount: async () => { | ||
const query: HogQLQuery = { | ||
kind: NodeKind.HogQLQuery, | ||
query: hogql`SELECT sum(count) as count | ||
FROM app_metrics | ||
WHERE app_source = 'metalytics' | ||
AND instance_id = ${values.instanceId}`, | ||
} | ||
|
||
// NOTE: I think this gets cached heavily - how to correctly invalidate? | ||
|
||
const response = await api.query(query) | ||
const result = response.results as number[] | ||
return result[0] | ||
}, | ||
}, | ||
], | ||
recentUsers: [ | ||
[] as string[], | ||
{ | ||
loadUsersLast30days: async () => { | ||
const query: HogQLQuery = { | ||
kind: NodeKind.HogQLQuery, | ||
query: hogql`SELECT DISTINCT app_source_id | ||
FROM app_metrics | ||
WHERE app_source = 'metalytics' | ||
AND instance_id = ${values.instanceId} | ||
AND timestamp >= NOW() - INTERVAL 30 DAY | ||
ORDER BY timestamp DESC`, | ||
} | ||
|
||
const response = await api.query(query) | ||
return response.results.map((result) => result[0]) as string[] | ||
}, | ||
}, | ||
], | ||
})), | ||
|
||
selectors({ | ||
instanceId: [ | ||
(s) => [s.sceneActivityFilters], | ||
(sceneActivityFilters) => | ||
sceneActivityFilters | ||
? sceneActivityFilters.item_id | ||
? `${sceneActivityFilters.scope}:${sceneActivityFilters.item_id}` | ||
: sceneActivityFilters.scope | ||
: null, | ||
], | ||
|
||
recentUserMembers: [ | ||
(s) => [s.recentUsers, s.members], | ||
(recentUsers, members) => { | ||
if (!members || !recentUsers) { | ||
return [] | ||
} | ||
// Filter members whose IDs match the recentUsers array | ||
const filteredMembers = members.filter((member) => recentUsers.includes(String(member.user.id))) | ||
return filteredMembers | ||
}, | ||
], | ||
}), | ||
|
||
subscriptions(({ actions }) => ({ | ||
instanceId: async (instanceId) => { | ||
if (instanceId) { | ||
actions.loadViewCount() | ||
actions.loadUsersLast30days() | ||
|
||
await api.create('/api/projects/@current/metalytics/', { | ||
metric_name: 'viewed', | ||
// metric_kind: 'misc', | ||
instance_id: instanceId, | ||
|
||
// API sets these | ||
// app_source: 'internal_metrics', | ||
// app_source_id: user.id, | ||
}) | ||
} | ||
}, | ||
})), | ||
]) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -232,6 +232,7 @@ export const FEATURE_FLAGS = { | |
CUSTOM_CHANNEL_TYPE_RULES: 'custom-channel-type-rules', // owner: @robbie-c #team-web-analytics | ||
SELF_SERVE_CREDIT_OVERRIDE: 'self-serve-credit-override', // owner: @zach | ||
EXPERIMENTS_MIGRATION_DISABLE_UI: 'experiments-migration-disable-ui', // owner: @jurajmajerik #team-experiments | ||
METALYTICS: 'metalytics', // owner: @surbhi | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't forget to add this to prod when shipping. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do i need to do anything additional to this to make sure its in prod? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} as const | ||
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you got it right here.