@ossy/analytics 3.0.9 → 3.4.0
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/README.md +30 -0
- package/package.json +15 -3
- package/src/Definition.js +1 -1
- package/src/analytics.page.jsx +67 -50
- package/src/en.translations.json +4 -1
- package/src/get-workspace-kpis.action.js +4 -0
- package/src/get-workspace-kpis.task.js +27 -0
- package/src/get-workspace-kpis.task.spec.js +61 -0
- package/src/index.js +1 -0
- package/src/sv.translations.json +4 -1
- package/src/useWorkspaceKpis.js +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @ossy/analytics
|
|
2
|
+
|
|
3
|
+
Analytics feature package for the Ossy platform. Shows **workspace-scoped** traffic and insight KPIs.
|
|
4
|
+
|
|
5
|
+
Platform-wide (ops) insights belong in a future back-office surface — see [ADR 0014](../../docs/adr/0014-system-insights-in-back-office.md).
|
|
6
|
+
|
|
7
|
+
## Pages
|
|
8
|
+
|
|
9
|
+
| Page | Path | Audience |
|
|
10
|
+
|------|------|----------|
|
|
11
|
+
| `analytics/home` | `/analytics` · `/analys` | Workspace members (entitled package) |
|
|
12
|
+
|
|
13
|
+
## Actions
|
|
14
|
+
|
|
15
|
+
| Action | Access | Notes |
|
|
16
|
+
|--------|--------|-------|
|
|
17
|
+
| `@ossy/analytics/actions/get-workspace-kpis` | `workspace` | Member count + non-removed resource count for the current workspace |
|
|
18
|
+
| `@ossy/resources/actions/get-page-view-stats` | `workspace` | Page-view aggregates (implemented in `@ossy/resources`) |
|
|
19
|
+
|
|
20
|
+
## Client usage
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { GetWorkspaceKpis } from '@ossy/analytics'
|
|
24
|
+
|
|
25
|
+
await sdk.invoke(GetWorkspaceKpis, {})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Preview
|
|
29
|
+
|
|
30
|
+
Enable Analytics in workspace entitlements (or `devEntitlements` in `@ossy/app-test`), then open `/analytics`.
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/analytics",
|
|
3
|
-
"description": "Analytics module — workspace
|
|
4
|
-
"version": "3.0
|
|
3
|
+
"description": "Analytics module — workspace insight KPIs",
|
|
4
|
+
"version": "3.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.js"
|
|
10
10
|
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose"
|
|
13
|
+
},
|
|
11
14
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
12
15
|
"license": "MIT",
|
|
13
16
|
"ossy": {
|
|
@@ -21,11 +24,20 @@
|
|
|
21
24
|
"/src",
|
|
22
25
|
"README.md"
|
|
23
26
|
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@ossy/event-store": "^3.4.0",
|
|
29
|
+
"@ossy/workspaces": "^3.4.0"
|
|
30
|
+
},
|
|
24
31
|
"peerDependencies": {
|
|
25
32
|
"@ossy/design-system": "*",
|
|
26
33
|
"@ossy/resources": "*",
|
|
27
34
|
"@ossy/sdk-react": "*",
|
|
35
|
+
"@ossy/workspaces": "*",
|
|
28
36
|
"react": "*"
|
|
29
37
|
},
|
|
30
|
-
"
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@jest/globals": "^30.2.0",
|
|
40
|
+
"jest": "^30.2.0"
|
|
41
|
+
},
|
|
42
|
+
"gitHead": "d36b69444268d172bc3e8e1dc77e67afc63f8650"
|
|
31
43
|
}
|
package/src/Definition.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const Definition = {
|
|
2
2
|
id: 'analytics',
|
|
3
3
|
title: 'Analytics',
|
|
4
|
-
description: 'Workspace
|
|
4
|
+
description: 'Workspace insight KPIs for members, resources, and traffic.',
|
|
5
5
|
icon: 'chart',
|
|
6
6
|
status: ['beta'],
|
|
7
7
|
}
|
package/src/analytics.page.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from 'react'
|
|
2
2
|
import { Text, View, Page, useLocale } from '@ossy/design-system'
|
|
3
3
|
import { usePageViewStats } from './usePageViewStats.js'
|
|
4
|
+
import { useWorkspaceKpis } from './useWorkspaceKpis.js'
|
|
4
5
|
import { useSdk } from '@ossy/sdk-react'
|
|
5
6
|
import { GetWorkspace } from '@ossy/workspaces'
|
|
6
7
|
|
|
@@ -35,12 +36,15 @@ export default function AnalyticsPage() {
|
|
|
35
36
|
return { from: now - 30 * 24 * 60 * 60 * 1000, to: now }
|
|
36
37
|
}, [])
|
|
37
38
|
|
|
38
|
-
const
|
|
39
|
+
const pageViews = usePageViewStats({
|
|
39
40
|
workspaceId: workspace?.id,
|
|
40
41
|
from,
|
|
41
42
|
to,
|
|
42
43
|
limit: 10,
|
|
43
44
|
})
|
|
45
|
+
const workspaceKpis = useWorkspaceKpis({ workspaceId: workspace?.id })
|
|
46
|
+
|
|
47
|
+
const { status, totalViews, dailyViews, topPaths } = pageViews
|
|
44
48
|
|
|
45
49
|
return (
|
|
46
50
|
<Page
|
|
@@ -48,63 +52,76 @@ export default function AnalyticsPage() {
|
|
|
48
52
|
description="analytics.home.description"
|
|
49
53
|
>
|
|
50
54
|
<View gap="m">
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
{(workspaceKpis.status === 'loading' || status === 'loading') && (
|
|
56
|
+
<View surface="primary" roundness="m" inset="m">
|
|
57
|
+
<Text>{t('analytics.home.loading')}</Text>
|
|
58
|
+
</View>
|
|
59
|
+
)}
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
{workspaceKpis.status === 'error' && (
|
|
62
|
+
<View surface="primary" roundness="m" inset="m">
|
|
63
|
+
<Text>{t('analytics.home.workspaceKpisError')}</Text>
|
|
64
|
+
</View>
|
|
65
|
+
)}
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
<>
|
|
67
|
+
{workspaceKpis.status === 'success' && (
|
|
65
68
|
<View layout="row" gap="m">
|
|
66
|
-
<SummaryCard label={t('analytics.home.
|
|
67
|
-
<SummaryCard label={t('analytics.home.
|
|
68
|
-
<SummaryCard label={t('analytics.home.daysWithTraffic')} value={`${dailyViews.length}`} />
|
|
69
|
+
<SummaryCard label={t('analytics.home.members')} value={`${workspaceKpis.members}`} />
|
|
70
|
+
<SummaryCard label={t('analytics.home.resources')} value={`${workspaceKpis.resources}`} />
|
|
69
71
|
</View>
|
|
72
|
+
)}
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
<Text variant="small">{item.path || t('analytics.home.unknownPath')}</Text>
|
|
84
|
-
<Text variant="small">{item.views}</Text>
|
|
85
|
-
</View>
|
|
86
|
-
))}
|
|
74
|
+
{status === 'error' && (
|
|
75
|
+
<View surface="primary" roundness="m" inset="m">
|
|
76
|
+
<Text>{t('analytics.home.error')}</Text>
|
|
77
|
+
</View>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{status === 'success' && (
|
|
81
|
+
<>
|
|
82
|
+
<View layout="row" gap="m">
|
|
83
|
+
<SummaryCard label={t('analytics.home.totalPageViews')} value={`${totalViews}`} />
|
|
84
|
+
<SummaryCard label={t('analytics.home.trackedPages')} value={`${topPaths.length}`} />
|
|
85
|
+
<SummaryCard label={t('analytics.home.daysWithTraffic')} value={`${dailyViews.length}`} />
|
|
87
86
|
</View>
|
|
88
87
|
|
|
89
|
-
<View
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
88
|
+
<View layout="row" gap="m" style={{ alignItems: 'flex-start' }}>
|
|
89
|
+
<View
|
|
90
|
+
surface="primary"
|
|
91
|
+
roundness="m"
|
|
92
|
+
inset="m"
|
|
93
|
+
gap="s"
|
|
94
|
+
style={{ border: '1px solid var(--separator)', flex: 1 }}
|
|
95
|
+
>
|
|
96
|
+
<Text variant="heading-secondary" as="h2" text="analytics.home.topPaths" />
|
|
97
|
+
{topPaths.length === 0 && <Text variant="small" text="analytics.home.noPageViews" />}
|
|
98
|
+
{topPaths.map((item) => (
|
|
99
|
+
<View key={item.path || 'unknown'} layout="row" justifyContent="space-between">
|
|
100
|
+
<Text variant="small">{item.path || t('analytics.home.unknownPath')}</Text>
|
|
101
|
+
<Text variant="small">{item.views}</Text>
|
|
102
|
+
</View>
|
|
103
|
+
))}
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
<View
|
|
107
|
+
surface="primary"
|
|
108
|
+
roundness="m"
|
|
109
|
+
inset="m"
|
|
110
|
+
gap="s"
|
|
111
|
+
style={{ border: '1px solid var(--separator)', flex: 1 }}
|
|
112
|
+
>
|
|
113
|
+
<Text variant="heading-secondary" as="h2" text="analytics.home.dailyPageViews" />
|
|
114
|
+
{dailyViews.length === 0 && <Text variant="small" text="analytics.home.noDailyData" />}
|
|
115
|
+
{dailyViews.map((item) => (
|
|
116
|
+
<View key={item.date} layout="row" justifyContent="space-between">
|
|
117
|
+
<Text variant="small">{item.date}</Text>
|
|
118
|
+
<Text variant="small">{item.views}</Text>
|
|
119
|
+
</View>
|
|
120
|
+
))}
|
|
121
|
+
</View>
|
|
104
122
|
</View>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)}
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
108
125
|
</View>
|
|
109
126
|
</Page>
|
|
110
127
|
)
|
package/src/en.translations.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"analytics/home.documentTitle": "Analytics",
|
|
3
3
|
"analytics.home.title": "Analytics",
|
|
4
|
-
"analytics.home.description": "Workspace
|
|
4
|
+
"analytics.home.description": "Workspace insight KPIs for members, resources, and traffic.",
|
|
5
5
|
"analytics.home.loading": "Loading analytics...",
|
|
6
6
|
"analytics.home.error": "Could not load analytics data.",
|
|
7
|
+
"analytics.home.members": "Members",
|
|
8
|
+
"analytics.home.resources": "Resources",
|
|
9
|
+
"analytics.home.workspaceKpisError": "Could not load workspace KPIs.",
|
|
7
10
|
"analytics.home.totalPageViews": "Total page views (30 days)",
|
|
8
11
|
"analytics.home.trackedPages": "Tracked pages (30 days)",
|
|
9
12
|
"analytics.home.daysWithTraffic": "Days with traffic",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Workspace } from '@ossy/workspaces/server'
|
|
3
|
+
|
|
4
|
+
export const metadata = { id: '@ossy/analytics/tasks/get-workspace-kpis' }
|
|
5
|
+
|
|
6
|
+
const RESOURCE_SCHEMA_TYPE = { $regex: /^@[^/]+\/[^/]+\/schema\// }
|
|
7
|
+
|
|
8
|
+
export async function run({ payload, req }) {
|
|
9
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
10
|
+
if (!workspaceId) throw Object.assign(new Error('workspaceId is required'), { status: 400 })
|
|
11
|
+
|
|
12
|
+
const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
|
|
13
|
+
const members = Array.isArray(workspace.users) ? workspace.users.length : 0
|
|
14
|
+
|
|
15
|
+
const resources = await Aggregate.Collection.countDocuments({
|
|
16
|
+
type: RESOURCE_SCHEMA_TYPE,
|
|
17
|
+
'state.belongsTo': workspace.id,
|
|
18
|
+
'state.status': { $ne: 'removed' },
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
scope: 'workspace',
|
|
23
|
+
workspaceId: workspace.id,
|
|
24
|
+
members,
|
|
25
|
+
resources,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
|
|
2
|
+
|
|
3
|
+
const countDocuments = jest.fn()
|
|
4
|
+
const ofMock = jest.fn()
|
|
5
|
+
const viewMock = jest.fn()
|
|
6
|
+
|
|
7
|
+
jest.unstable_mockModule('@ossy/event-store', () => ({
|
|
8
|
+
Aggregate: {
|
|
9
|
+
Of: ofMock,
|
|
10
|
+
View: viewMock,
|
|
11
|
+
Collection: { countDocuments },
|
|
12
|
+
},
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
jest.unstable_mockModule('@ossy/workspaces/server', () => ({
|
|
16
|
+
Workspace: { name: 'Workspace' },
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
const { run } = await import('./get-workspace-kpis.task.js')
|
|
20
|
+
|
|
21
|
+
describe('get-workspace-kpis task', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
countDocuments.mockReset()
|
|
24
|
+
ofMock.mockReset()
|
|
25
|
+
viewMock.mockReset()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('requires workspaceId', async () => {
|
|
29
|
+
await expect(run({ payload: {}, req: {} })).rejects.toMatchObject({
|
|
30
|
+
message: 'workspaceId is required',
|
|
31
|
+
status: 400,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('returns member and resource counts for the workspace', async () => {
|
|
36
|
+
ofMock.mockReturnValue(Promise.resolve({ id: 'ws-1' }))
|
|
37
|
+
viewMock.mockReturnValue(() => ({ id: 'ws-1', users: ['u1', 'u2', 'u3'] }))
|
|
38
|
+
countDocuments.mockResolvedValue(7)
|
|
39
|
+
|
|
40
|
+
// Aggregate.Of(...).then(Aggregate.View()) — View() returns a function applied by then
|
|
41
|
+
ofMock.mockImplementation(() => ({
|
|
42
|
+
then(fn) {
|
|
43
|
+
return Promise.resolve(fn({ id: 'ws-1', users: ['u1', 'u2', 'u3'] }))
|
|
44
|
+
},
|
|
45
|
+
}))
|
|
46
|
+
viewMock.mockReturnValue((saved) => saved)
|
|
47
|
+
|
|
48
|
+
const result = await run({ payload: { workspaceId: 'ws-1' }, req: {} })
|
|
49
|
+
|
|
50
|
+
expect(result).toEqual({
|
|
51
|
+
scope: 'workspace',
|
|
52
|
+
workspaceId: 'ws-1',
|
|
53
|
+
members: 3,
|
|
54
|
+
resources: 7,
|
|
55
|
+
})
|
|
56
|
+
expect(countDocuments).toHaveBeenCalledWith(expect.objectContaining({
|
|
57
|
+
'state.belongsTo': 'ws-1',
|
|
58
|
+
'state.status': { $ne: 'removed' },
|
|
59
|
+
}))
|
|
60
|
+
})
|
|
61
|
+
})
|
package/src/index.js
CHANGED
package/src/sv.translations.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"analytics/home.documentTitle": "Analys",
|
|
3
3
|
"analytics.home.title": "Analys",
|
|
4
|
-
"analytics.home.description": "Workspace-
|
|
4
|
+
"analytics.home.description": "Workspace-KPIer för medlemmar, resurser och trafik.",
|
|
5
5
|
"analytics.home.loading": "Laddar analys...",
|
|
6
6
|
"analytics.home.error": "Kunde inte ladda analysdata.",
|
|
7
|
+
"analytics.home.members": "Medlemmar",
|
|
8
|
+
"analytics.home.resources": "Resurser",
|
|
9
|
+
"analytics.home.workspaceKpisError": "Kunde inte ladda workspace-KPIer.",
|
|
7
10
|
"analytics.home.totalPageViews": "Totala sidvisningar (30 dagar)",
|
|
8
11
|
"analytics.home.trackedPages": "Spårade sidor (30 dagar)",
|
|
9
12
|
"analytics.home.daysWithTraffic": "Dagar med trafik",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { useSdk } from '@ossy/sdk-react'
|
|
3
|
+
import { metadata as GetWorkspaceKpis } from './get-workspace-kpis.action.js'
|
|
4
|
+
|
|
5
|
+
export function useWorkspaceKpis({ workspaceId }) {
|
|
6
|
+
const sdk = useSdk()
|
|
7
|
+
const [status, setStatus] = useState('idle')
|
|
8
|
+
const [data, setData] = useState({
|
|
9
|
+
members: 0,
|
|
10
|
+
resources: 0,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!workspaceId) return
|
|
15
|
+
|
|
16
|
+
setStatus('loading')
|
|
17
|
+
sdk.invoke(GetWorkspaceKpis, {})
|
|
18
|
+
.then((json) => {
|
|
19
|
+
setData({
|
|
20
|
+
members: json?.members || 0,
|
|
21
|
+
resources: json?.resources || 0,
|
|
22
|
+
})
|
|
23
|
+
setStatus('success')
|
|
24
|
+
})
|
|
25
|
+
.catch(() => {
|
|
26
|
+
setStatus('error')
|
|
27
|
+
})
|
|
28
|
+
}, [sdk, workspaceId])
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
status,
|
|
32
|
+
...data,
|
|
33
|
+
}
|
|
34
|
+
}
|