@ossy/analytics 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -2
- package/src/analytics.page.jsx +112 -0
- package/src/usePageViewStats.js +52 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/analytics",
|
|
3
3
|
"description": "Analytics module — workspace analytics for website traffic",
|
|
4
|
-
"version": "1.1
|
|
4
|
+
"version": "1.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
@@ -21,5 +21,10 @@
|
|
|
21
21
|
"/src",
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
|
-
"
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@ossy/design-system": "*",
|
|
26
|
+
"@ossy/sdk-react": "*",
|
|
27
|
+
"react": "*"
|
|
28
|
+
},
|
|
29
|
+
"gitHead": "b976fdb88970a76f135238baa970c20f9f357dc9"
|
|
25
30
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import { useWorkspace } from '@ossy/sdk-react'
|
|
3
|
+
import { Definition } from '@ossy/analytics'
|
|
4
|
+
import { Text, Title, View } from '@ossy/design-system'
|
|
5
|
+
import { usePageViewStats } from './usePageViewStats.js'
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
id: 'analytics/home',
|
|
9
|
+
title: 'Analytics',
|
|
10
|
+
path: {
|
|
11
|
+
sv: '/analys',
|
|
12
|
+
en: '/analytics',
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function SummaryCard({ label, value }) {
|
|
17
|
+
return (
|
|
18
|
+
<View
|
|
19
|
+
surface="primary"
|
|
20
|
+
roundness="m"
|
|
21
|
+
inset="m"
|
|
22
|
+
style={{ border: '1px solid var(--separator)' }}
|
|
23
|
+
>
|
|
24
|
+
<Text variant="small" style={{ opacity: 0.75 }}>{label}</Text>
|
|
25
|
+
<Title variant="secondary">{value}</Title>
|
|
26
|
+
</View>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function AnalyticsPage() {
|
|
31
|
+
const { workspace } = useWorkspace()
|
|
32
|
+
// Snapshot once per mount: Date.now() on every render changed `from`/`to` every tick and
|
|
33
|
+
// retriggered usePageViewStats (params → useEffect) in a fetch/setState loop.
|
|
34
|
+
const { from, to } = useMemo(() => {
|
|
35
|
+
const t = Date.now()
|
|
36
|
+
return { from: t - 30 * 24 * 60 * 60 * 1000, to: t }
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
const { status, totalViews, dailyViews, topPaths } = usePageViewStats({
|
|
40
|
+
workspaceId: workspace?.id,
|
|
41
|
+
from,
|
|
42
|
+
to,
|
|
43
|
+
limit: 10,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View inset="l" gap="l" style={{ width: '100%', boxSizing: 'border-box' }}>
|
|
48
|
+
<View gap="s">
|
|
49
|
+
<Title>{Definition.title}</Title>
|
|
50
|
+
<Text>{Definition.description}</Text>
|
|
51
|
+
</View>
|
|
52
|
+
|
|
53
|
+
{status === 'loading' && (
|
|
54
|
+
<View surface="primary" roundness="m" inset="m">
|
|
55
|
+
<Text>Loading analytics...</Text>
|
|
56
|
+
</View>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{status === 'error' && (
|
|
60
|
+
<View surface="primary" roundness="m" inset="m">
|
|
61
|
+
<Text>Could not load analytics data.</Text>
|
|
62
|
+
</View>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{status === 'success' && (
|
|
66
|
+
<>
|
|
67
|
+
<View layout="row" gap="m">
|
|
68
|
+
<SummaryCard label="Total page views (30 days)" value={`${totalViews}`} />
|
|
69
|
+
<SummaryCard label="Tracked pages (30 days)" value={`${topPaths.length}`} />
|
|
70
|
+
<SummaryCard label="Days with traffic" value={`${dailyViews.length}`} />
|
|
71
|
+
</View>
|
|
72
|
+
|
|
73
|
+
<View layout="row" gap="m" style={{ alignItems: 'flex-start' }}>
|
|
74
|
+
<View
|
|
75
|
+
surface="primary"
|
|
76
|
+
roundness="m"
|
|
77
|
+
inset="m"
|
|
78
|
+
gap="s"
|
|
79
|
+
style={{ border: '1px solid var(--separator)', flex: 1 }}
|
|
80
|
+
>
|
|
81
|
+
<Title variant="secondary">Top paths</Title>
|
|
82
|
+
{topPaths.length === 0 && <Text variant="small">No page views yet.</Text>}
|
|
83
|
+
{topPaths.map((item) => (
|
|
84
|
+
<View key={item.path || 'unknown'} layout="row" justifyContent="space-between">
|
|
85
|
+
<Text variant="small">{item.path || '(unknown path)'}</Text>
|
|
86
|
+
<Text variant="small">{item.views}</Text>
|
|
87
|
+
</View>
|
|
88
|
+
))}
|
|
89
|
+
</View>
|
|
90
|
+
|
|
91
|
+
<View
|
|
92
|
+
surface="primary"
|
|
93
|
+
roundness="m"
|
|
94
|
+
inset="m"
|
|
95
|
+
gap="s"
|
|
96
|
+
style={{ border: '1px solid var(--separator)', flex: 1 }}
|
|
97
|
+
>
|
|
98
|
+
<Title variant="secondary">Daily page views</Title>
|
|
99
|
+
{dailyViews.length === 0 && <Text variant="small">No daily data yet.</Text>}
|
|
100
|
+
{dailyViews.map((item) => (
|
|
101
|
+
<View key={item.date} layout="row" justifyContent="space-between">
|
|
102
|
+
<Text variant="small">{item.date}</Text>
|
|
103
|
+
<Text variant="small">{item.views}</Text>
|
|
104
|
+
</View>
|
|
105
|
+
))}
|
|
106
|
+
</View>
|
|
107
|
+
</View>
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
</View>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
const API_URL = '/@ossy'
|
|
4
|
+
|
|
5
|
+
export function usePageViewStats({ workspaceId, from, to, limit = 10 }) {
|
|
6
|
+
const [status, setStatus] = useState('loading')
|
|
7
|
+
const [data, setData] = useState({
|
|
8
|
+
totalViews: 0,
|
|
9
|
+
dailyViews: [],
|
|
10
|
+
topPaths: [],
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const params = useMemo(() => {
|
|
14
|
+
const sp = new URLSearchParams()
|
|
15
|
+
if (from) sp.set('from', `${from}`)
|
|
16
|
+
if (to) sp.set('to', `${to}`)
|
|
17
|
+
if (limit) sp.set('limit', `${limit}`)
|
|
18
|
+
return sp.toString()
|
|
19
|
+
}, [from, to, limit])
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!workspaceId) return
|
|
23
|
+
|
|
24
|
+
setStatus('loading')
|
|
25
|
+
fetch(`${API_URL}/resources/page-views/stats?${params}`, {
|
|
26
|
+
method: 'GET',
|
|
27
|
+
headers: {
|
|
28
|
+
workspaceId,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
.then(res => {
|
|
32
|
+
if (!res.ok) return Promise.reject(new Error('Failed to fetch stats'))
|
|
33
|
+
return res.json()
|
|
34
|
+
})
|
|
35
|
+
.then(json => {
|
|
36
|
+
setData({
|
|
37
|
+
totalViews: json?.totalViews || 0,
|
|
38
|
+
dailyViews: json?.dailyViews || [],
|
|
39
|
+
topPaths: json?.topPaths || [],
|
|
40
|
+
})
|
|
41
|
+
setStatus('success')
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {
|
|
44
|
+
setStatus('error')
|
|
45
|
+
})
|
|
46
|
+
}, [workspaceId, params])
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
status,
|
|
50
|
+
...data,
|
|
51
|
+
}
|
|
52
|
+
}
|