user stats

This commit is contained in:
keyan 2022-06-24 10:38:00 -05:00
parent 2c749dd07f
commit 0b3b690c10
7 changed files with 431 additions and 101 deletions

View File

@ -20,21 +20,30 @@ export default {
}, },
itemGrowth: async (parent, args, { models }) => { itemGrowth: async (parent, args, { models }) => {
return await models.$queryRaw( return await models.$queryRaw(
`SELECT date_trunc('month', created_at) AS time, count(*) as num `SELECT date_trunc('month', created_at) AS time, count("parentId") as comments,
count("subName") as jobs, count(*)-count("parentId")-count("subName") as posts
FROM "Item" FROM "Item"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at) WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at)
GROUP BY time GROUP BY time
ORDER BY time ASC`) ORDER BY time ASC`)
}, },
spentGrowth: async (parent, args, { models }) => { spentGrowth: async (parent, args, { models }) => {
// add up earn for each month
// add up non-self votes/tips for posts and comments
return await models.$queryRaw( return await models.$queryRaw(
`SELECT date_trunc('month', created_at) AS time, sum(sats) as num `SELECT date_trunc('month', "ItemAct".created_at) AS time,
sum(CASE WHEN act = 'STREAM' THEN sats ELSE 0 END) as jobs,
sum(CASE WHEN act = 'VOTE' AND "Item"."userId" = "ItemAct"."userId" THEN sats ELSE 0 END) as fees,
sum(CASE WHEN act = 'BOOST' THEN sats ELSE 0 END) as boost,
sum(CASE WHEN act = 'TIP' THEN sats ELSE 0 END) as tips
FROM "ItemAct" FROM "ItemAct"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at) JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE date_trunc('month', now_utc()) <> date_trunc('month', "ItemAct".created_at)
GROUP BY time GROUP BY time
ORDER BY time ASC`) ORDER BY time ASC`)
}, },
earnedGrowth: async (parent, args, { models }) => { earnerGrowth: async (parent, args, { models }) => {
return await models.$queryRaw( return await models.$queryRaw(
`SELECT time, count(distinct user_id) as num `SELECT time, count(distinct user_id) as num
FROM FROM
@ -48,6 +57,101 @@ export default {
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at))) u WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at))) u
GROUP BY time GROUP BY time
ORDER BY time ASC`) ORDER BY time ASC`)
},
stackedGrowth: async (parent, args, { models }) => {
return await models.$queryRaw(
`SELECT time, sum(airdrop) as airdrops, sum(post) as posts, sum(comment) as comments
FROM
((SELECT date_trunc('month', "ItemAct".created_at) AS time, 0 as airdrop,
CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE sats END as comment,
CASE WHEN "Item"."parentId" IS NULL THEN sats ELSE 0 END as post
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', "ItemAct".created_at) AND
"ItemAct".act IN ('VOTE', 'TIP'))
UNION ALL
(SELECT date_trunc('month', created_at) AS time, msats / 1000 as airdrop, 0 as post, 0 as comment
FROM "Earn"
WHERE date_trunc('month', now_utc()) <> date_trunc('month', created_at))) u
GROUP BY time
ORDER BY time ASC`)
},
registrationsWeekly: async (parent, args, { models }) => {
return await models.item.count({
where: {
createdAt: {
gte: new Date(new Date().setDate(new Date().getDate() - 7))
}
}
})
},
activeWeekly: async (parent, args, { models }) => {
const [{ active }] = await models.$queryRaw(
`SELECT count(DISTINCT "userId") as active
FROM "ItemAct"
WHERE created_at >= now_utc() - interval '1 week'`
)
return active
},
earnersWeekly: async (parent, args, { models }) => {
const [{ earners }] = await models.$queryRaw(
`SELECT count(distinct user_id) as earners
FROM
((SELECT "Item"."userId" as user_id
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId"
WHERE "ItemAct".created_at >= now_utc() - interval '1 week')
UNION ALL
(SELECT "userId" as user_id
FROM "Earn"
WHERE created_at >= now_utc() - interval '1 week')) u`)
return earners
},
itemsWeekly: async (parent, args, { models }) => {
const [stats] = await models.$queryRaw(
`SELECT json_build_array(
json_build_object('name', 'comments', 'value', count("parentId")),
json_build_object('name', 'job', 'value', count("subName")),
json_build_object('name', 'posts', 'value', count(*)-count("parentId")-count("subName"))) as array
FROM "Item"
WHERE created_at >= now_utc() - interval '1 week'`)
return stats?.array
},
spentWeekly: async (parent, args, { models }) => {
const [stats] = await models.$queryRaw(
`SELECT json_build_array(
json_build_object('name', 'jobs', 'value', sum(CASE WHEN act = 'STREAM' THEN sats ELSE 0 END)),
json_build_object('name', 'fees', 'value', sum(CASE WHEN act = 'VOTE' AND "Item"."userId" = "ItemAct"."userId" THEN sats ELSE 0 END)),
json_build_object('name', 'boost', 'value', sum(CASE WHEN act = 'BOOST' THEN sats ELSE 0 END)),
json_build_object('name', 'tips', 'value', sum(CASE WHEN act = 'TIP' THEN sats ELSE 0 END))) as array
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE "ItemAct".created_at >= now_utc() - interval '1 week'`)
return stats?.array
},
stackedWeekly: async (parent, args, { models }) => {
const [stats] = await models.$queryRaw(
`SELECT json_build_array(
json_build_object('name', 'airdrops', 'value', sum(airdrop)),
json_build_object('name', 'posts', 'value', sum(post)),
json_build_object('name', 'comments', 'value', sum(comment))
) as array
FROM
((SELECT 0 as airdrop,
CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE sats END as comment,
CASE WHEN "Item"."parentId" IS NULL THEN sats ELSE 0 END as post
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id AND "Item"."userId" <> "ItemAct"."userId"
WHERE "ItemAct".created_at >= now_utc() - interval '1 week' AND
"ItemAct".act IN ('VOTE', 'TIP'))
UNION ALL
(SELECT msats / 1000 as airdrop, 0 as post, 0 as comment
FROM "Earn"
WHERE created_at >= now_utc() - interval '1 week')) u`)
return stats?.array
} }
} }
} }

View File

@ -4,13 +4,48 @@ export default gql`
extend type Query { extend type Query {
registrationGrowth: [TimeNum!]! registrationGrowth: [TimeNum!]!
activeGrowth: [TimeNum!]! activeGrowth: [TimeNum!]!
itemGrowth: [TimeNum!]! itemGrowth: [ItemGrowth!]!
spentGrowth: [TimeNum!]! spentGrowth: [SpentGrowth!]!
earnedGrowth: [TimeNum!]! stackedGrowth: [StackedGrowth!]!
earnerGrowth: [TimeNum!]!
registrationsWeekly: Int!
activeWeekly: Int!
earnersWeekly: Int!
itemsWeekly: [NameValue!]!
spentWeekly: [NameValue!]!
stackedWeekly: [NameValue!]!
} }
type TimeNum { type TimeNum {
time: String! time: String!
num: Int! num: Int!
} }
type NameValue {
name: String!
value: Int!
}
type ItemGrowth {
time: String!
jobs: Int!
posts: Int!
comments: Int!
}
type StackedGrowth {
time: String!
airdrops: Int!
posts: Int!
comments: Int!
}
type SpentGrowth {
time: String!
jobs: Int!
fees: Int!
boost: Int!
tips: Int!
}
` `

View File

@ -96,9 +96,9 @@ const AnalyticsPopover = (
visitors visitors
</a> </a>
<span className='mx-2 text-dark'> \ </span> <span className='mx-2 text-dark'> \ </span>
<Link href='/usage' passHref> <Link href='/users/forever' passHref>
<a className='text-dark d-inline-flex'> <a className='text-dark d-inline-flex'>
usage users
</a> </a>
</Link> </Link>
</Popover.Content> </Popover.Content>

View File

@ -0,0 +1,35 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Nav, Navbar } from 'react-bootstrap'
import styles from './header.module.css'
export function UsageHeader () {
const router = useRouter()
return (
<Navbar className='pt-0'>
<Nav
className={`${styles.navbarNav} justify-content-around`}
activeKey={router.asPath}
>
<Nav.Item>
<Link href='/users/week' passHref>
<Nav.Link
className={styles.navLink}
>
week
</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Link href='/users/forever' passHref>
<Nav.Link
className={styles.navLink}
>
forever
</Nav.Link>
</Link>
</Nav.Item>
</Nav>
</Navbar>
)
}

View File

@ -1,92 +0,0 @@
import { gql } from '@apollo/client'
import { getGetServerSideProps } from '../api/ssrApollo'
import Layout from '../components/layout'
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer } from 'recharts'
import { Col, Row } from 'react-bootstrap'
import { formatSats } from '../lib/format'
export const getServerSideProps = getGetServerSideProps(
gql`
{
registrationGrowth {
time
num
}
activeGrowth {
time
num
}
itemGrowth {
time
num
}
spentGrowth {
time
num
}
earnedGrowth {
time
num
}
}`)
const dateFormatter = timeStr => {
const date = new Date(timeStr)
return `${('0' + (date.getMonth() + 2)).slice(-2)}/${String(date.getFullYear()).slice(-2)}`
}
export default function Growth ({
data: { registrationGrowth, activeGrowth, itemGrowth, spentGrowth, earnedGrowth }
}) {
return (
<Layout>
<Row className='mt-3'>
<Col>
<GrowthLineChart data={registrationGrowth} xName='month' yName='registrations' />
</Col>
<Col>
<GrowthLineChart data={activeGrowth} xName='month' yName='active users' />
</Col>
</Row>
<Row className='mt-3'>
<Col>
<GrowthLineChart data={itemGrowth} xName='month' yName='items' />
</Col>
<Col>
<GrowthLineChart data={spentGrowth} xName='month' yName='sats spent' />
</Col>
</Row>
<Row className='mt-3'>
<Col>
<GrowthLineChart data={earnedGrowth} xName='month' yName='earning users' />
</Col>
<Col />
</Row>
</Layout>
)
}
function GrowthLineChart ({ data, xName, yName }) {
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
<LineChart
data={data}
margin={{
top: 5,
right: 5,
left: 0,
bottom: 0
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter} name={xName}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis tickFormatter={formatSats} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
<Legend />
<Line type='monotone' dataKey='num' name={yName} stroke='var(--secondary)' />
</LineChart>
</ResponsiveContainer>
)
}

147
pages/users/forever.js Normal file
View File

@ -0,0 +1,147 @@
import { gql } from '@apollo/client'
import { getGetServerSideProps } from '../../api/ssrApollo'
import Layout from '../../components/layout'
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts'
import { Col, Row } from 'react-bootstrap'
import { formatSats } from '../../lib/format'
import { UsageHeader } from '../../components/usage-header'
export const getServerSideProps = getGetServerSideProps(
gql`
{
registrationGrowth {
time
num
}
activeGrowth {
time
num
}
itemGrowth {
time
jobs
comments
posts
}
spentGrowth {
time
jobs
fees
boost
tips
}
stackedGrowth {
time
posts
comments
airdrops
}
earnerGrowth {
time
num
}
}`)
const dateFormatter = timeStr => {
const date = new Date(timeStr)
return `${('0' + (date.getMonth() + 2)).slice(-2)}/${String(date.getFullYear()).slice(-2)}`
}
export default function Growth ({
data: { registrationGrowth, activeGrowth, itemGrowth, spentGrowth, earnerGrowth, stackedGrowth }
}) {
return (
<Layout>
<UsageHeader />
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold invisible'>earning users</div>
<GrowthLineChart data={earnerGrowth} xName='month' yName='earning users' />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>stacking</div>
<GrowthAreaChart data={stackedGrowth} xName='month' />
</Col>
</Row>
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>items</div>
<GrowthAreaChart data={itemGrowth} xName='month' />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>spending</div>
<GrowthAreaChart data={spentGrowth} xName='month' yName='sats spent' />
</Col>
</Row>
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold invisible'>registrations</div>
<GrowthLineChart data={registrationGrowth} xName='month' yName='registrations' />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold invisible'>active users</div>
<GrowthLineChart data={activeGrowth} xName='month' yName='interactive users' />
</Col>
</Row>
</Layout>
)
}
const COLORS = [
'var(--secondary)',
'var(--info)',
'var(--success)',
'var(--boost)',
'var(--grey)'
]
function GrowthAreaChart ({ data, xName, title }) {
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
<AreaChart
data={data}
margin={{
top: 5,
right: 5,
left: 0,
bottom: 0
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter} name={xName}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis tickFormatter={formatSats} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
<Legend />
{Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
<Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
</AreaChart>
</ResponsiveContainer>
)
}
function GrowthLineChart ({ data, xName, yName }) {
return (
<ResponsiveContainer width='100%' height={300} minWidth={300}>
<LineChart
data={data}
margin={{
top: 5,
right: 5,
left: 0,
bottom: 0
}}
>
<XAxis
dataKey='time' tickFormatter={dateFormatter} name={xName}
tick={{ fill: 'var(--theme-grey)' }}
/>
<YAxis tickFormatter={formatSats} tick={{ fill: 'var(--theme-grey)' }} />
<Tooltip labelFormatter={dateFormatter} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
<Legend />
<Line type='monotone' dataKey='num' name={yName} stroke='var(--secondary)' />
</LineChart>
</ResponsiveContainer>
)
}

101
pages/users/week.js Normal file
View File

@ -0,0 +1,101 @@
import { gql } from '@apollo/client'
import { getGetServerSideProps } from '../../api/ssrApollo'
import Layout from '../../components/layout'
import { Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
import { Col, Row } from 'react-bootstrap'
import { UsageHeader } from '../../components/usage-header'
export const getServerSideProps = getGetServerSideProps(
gql`
{
registrationsWeekly
activeWeekly
earnersWeekly
itemsWeekly {
name
value
}
spentWeekly {
name
value
}
stackedWeekly {
name
value
}
}`)
export default function Growth ({
data: {
registrationsWeekly, activeWeekly, itemsWeekly, spentWeekly,
stackedWeekly, earnersWeekly
}
}) {
return (
<Layout>
<UsageHeader />
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>registrations</div>
<h3 className='text-center'>{registrationsWeekly}</h3>
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>interactive users</div>
<h3 className='text-center'>{activeWeekly}</h3>
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>earners</div>
<h3 className='text-center'>{earnersWeekly}</h3>
</Col>
</Row>
<Row>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>items</div>
<GrowthPieChart data={itemsWeekly} />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>stacked</div>
<GrowthPieChart data={stackedWeekly} />
</Col>
<Col className='mt-3'>
<div className='text-center text-muted font-weight-bold'>spent</div>
<GrowthPieChart data={spentWeekly} />
</Col>
</Row>
</Layout>
)
}
const COLORS = [
'var(--secondary)',
'var(--info)',
'var(--success)',
'var(--boost)',
'var(--grey)'
]
function GrowthPieChart ({ data }) {
return (
<ResponsiveContainer width='100%' height={250} minWidth={200}>
<PieChart margin={{ top: 5, right: 5, bottom: 5, left: 5 }}>
<Pie
dataKey='value'
isAnimationActive={false}
data={data}
cx='50%'
cy='50%'
outerRadius={80}
fill='var(--secondary)'
label
>
{
data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index]} />
))
}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
)
}