@ossy/analytics 3.0.9 → 3.5.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 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` | Signed-in + entitled → product KPIs. Otherwise the public sales page (no KPI loads). |
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 analytics for website traffic",
4
- "version": "3.0.9",
3
+ "description": "Analytics module — workspace insight KPIs",
4
+ "version": "3.5.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,23 @@
21
24
  "/src",
22
25
  "README.md"
23
26
  ],
27
+ "dependencies": {
28
+ "@ossy/event-store": "^3.4.0",
29
+ "@ossy/workspaces": "^3.5.0"
30
+ },
24
31
  "peerDependencies": {
32
+ "@ossy/app": "*",
33
+ "@ossy/authentication": "*",
25
34
  "@ossy/design-system": "*",
26
35
  "@ossy/resources": "*",
36
+ "@ossy/router-react": "*",
27
37
  "@ossy/sdk-react": "*",
38
+ "@ossy/workspaces": "*",
28
39
  "react": "*"
29
40
  },
30
- "gitHead": "908fae28675f6c3c179d4c2eecb451e8a1af886e"
41
+ "devDependencies": {
42
+ "@jest/globals": "^30.2.0",
43
+ "jest": "^30.2.0"
44
+ },
45
+ "gitHead": "23167600f991596f122765a53f2b9df08314e115"
31
46
  }
@@ -0,0 +1,124 @@
1
+ import React, { useMemo } from 'react'
2
+ import { Text, View, Page, useLocale } from '@ossy/design-system'
3
+ import { usePageViewStats } from './usePageViewStats.js'
4
+ import { useWorkspaceKpis } from './useWorkspaceKpis.js'
5
+ import { useSdk } from '@ossy/sdk-react'
6
+ import { GetWorkspace } from '@ossy/workspaces'
7
+
8
+ function SummaryCard ({ label, value }) {
9
+ return (
10
+ <View
11
+ surface="primary"
12
+ roundness="m"
13
+ inset="m"
14
+ style={{ border: '1px solid var(--separator)' }}
15
+ >
16
+ <Text variant="small" style={{ opacity: 0.75 }}>{label}</Text>
17
+ <Text variant="heading-secondary" as="h2">{value}</Text>
18
+ </View>
19
+ )
20
+ }
21
+
22
+ /**
23
+ * Workspace analytics product UI. Only mount when the caller is authenticated
24
+ * so KPI / page-view actions do not run for anonymous sessions.
25
+ */
26
+ export default function AnalyticsProductHome () {
27
+ const { t } = useLocale()
28
+ const sdk = useSdk()
29
+ const { data: workspace } = sdk.read(GetWorkspace)
30
+ const { from, to } = useMemo(() => {
31
+ const now = Date.now()
32
+ return { from: now - 30 * 24 * 60 * 60 * 1000, to: now }
33
+ }, [])
34
+
35
+ const pageViews = usePageViewStats({
36
+ workspaceId: workspace?.id,
37
+ from,
38
+ to,
39
+ limit: 10,
40
+ })
41
+ const workspaceKpis = useWorkspaceKpis({ workspaceId: workspace?.id })
42
+
43
+ const { status, totalViews, dailyViews, topPaths } = pageViews
44
+
45
+ return (
46
+ <Page
47
+ title="analytics.home.title"
48
+ description="analytics.home.description"
49
+ >
50
+ <View gap="m">
51
+ {(workspaceKpis.status === 'loading' || status === 'loading') && (
52
+ <View surface="primary" roundness="m" inset="m">
53
+ <Text>{t('analytics.home.loading')}</Text>
54
+ </View>
55
+ )}
56
+
57
+ {workspaceKpis.status === 'error' && (
58
+ <View surface="primary" roundness="m" inset="m">
59
+ <Text>{t('analytics.home.workspaceKpisError')}</Text>
60
+ </View>
61
+ )}
62
+
63
+ {workspaceKpis.status === 'success' && (
64
+ <View layout="row" gap="m">
65
+ <SummaryCard label={t('analytics.home.members')} value={`${workspaceKpis.members}`} />
66
+ <SummaryCard label={t('analytics.home.resources')} value={`${workspaceKpis.resources}`} />
67
+ </View>
68
+ )}
69
+
70
+ {status === 'error' && (
71
+ <View surface="primary" roundness="m" inset="m">
72
+ <Text>{t('analytics.home.error')}</Text>
73
+ </View>
74
+ )}
75
+
76
+ {status === 'success' && (
77
+ <>
78
+ <View layout="row" gap="m">
79
+ <SummaryCard label={t('analytics.home.totalPageViews')} value={`${totalViews}`} />
80
+ <SummaryCard label={t('analytics.home.trackedPages')} value={`${topPaths.length}`} />
81
+ <SummaryCard label={t('analytics.home.daysWithTraffic')} value={`${dailyViews.length}`} />
82
+ </View>
83
+
84
+ <View layout="row" gap="m" style={{ alignItems: 'flex-start' }}>
85
+ <View
86
+ surface="primary"
87
+ roundness="m"
88
+ inset="m"
89
+ gap="s"
90
+ style={{ border: '1px solid var(--separator)', flex: 1 }}
91
+ >
92
+ <Text variant="heading-secondary" as="h2" text="analytics.home.topPaths" />
93
+ {topPaths.length === 0 && <Text variant="small" text="analytics.home.noPageViews" />}
94
+ {topPaths.map((item) => (
95
+ <View key={item.path || 'unknown'} layout="row" justifyContent="space-between">
96
+ <Text variant="small">{item.path || t('analytics.home.unknownPath')}</Text>
97
+ <Text variant="small">{item.views}</Text>
98
+ </View>
99
+ ))}
100
+ </View>
101
+
102
+ <View
103
+ surface="primary"
104
+ roundness="m"
105
+ inset="m"
106
+ gap="s"
107
+ style={{ border: '1px solid var(--separator)', flex: 1 }}
108
+ >
109
+ <Text variant="heading-secondary" as="h2" text="analytics.home.dailyPageViews" />
110
+ {dailyViews.length === 0 && <Text variant="small" text="analytics.home.noDailyData" />}
111
+ {dailyViews.map((item) => (
112
+ <View key={item.date} layout="row" justifyContent="space-between">
113
+ <Text variant="small">{item.date}</Text>
114
+ <Text variant="small">{item.views}</Text>
115
+ </View>
116
+ ))}
117
+ </View>
118
+ </View>
119
+ </>
120
+ )}
121
+ </View>
122
+ </Page>
123
+ )
124
+ }
@@ -0,0 +1,55 @@
1
+ import React, { useState, useCallback } from 'react'
2
+ import { GetWorkspace, EnableService } from '@ossy/workspaces'
3
+ import { useSdk, cacheKey } from '@ossy/sdk-react'
4
+ import { Text, View, Button, Page, useLocale } from '@ossy/design-system'
5
+ import SalesSection from './SalesSection.jsx'
6
+ import { useAnalyticsHomeContent } from './analytics-home-content.js'
7
+
8
+ const ANALYTICS_SERVICE = '@ossy/analytics'
9
+
10
+ /**
11
+ * Public sales surface. Anonymous → sign up; signed-in but not entitled → enable.
12
+ * Does not load workspace KPI / page-view actions.
13
+ */
14
+ export default function AnalyticsSalesPage ({ isAuthenticated }) {
15
+ const { t } = useLocale()
16
+ const { invoke, invalidate } = useSdk()
17
+ const [error, setError] = useState(null)
18
+ const { cover: baseCover, features } = useAnalyticsHomeContent()
19
+
20
+ const handleEnable = useCallback(() => {
21
+ setError(null)
22
+ invoke(EnableService, { service: ANALYTICS_SERVICE })
23
+ .then(() => invalidate(cacheKey(GetWorkspace)))
24
+ .catch(() => setError(t('analytics.sales.enableError')))
25
+ }, [invoke, invalidate, t])
26
+
27
+ const enableAction = {
28
+ ...EnableService,
29
+ 'data-service': ANALYTICS_SERVICE,
30
+ variant: 'cta',
31
+ label: 'analytics.sales.enable',
32
+ onClick: handleEnable,
33
+ }
34
+
35
+ const cover = isAuthenticated
36
+ ? { ...baseCover, actions: [] }
37
+ : baseCover
38
+
39
+ return (
40
+ <View data-analytics-status="off">
41
+ <SalesSection cover={cover} features={features} />
42
+
43
+ {isAuthenticated && (
44
+ <Page maxWidth="xl" gap="l">
45
+ <View gap="m" inset="l">
46
+ <Text variant="heading-secondary" as="h2" text="analytics.sales.enable" />
47
+ <Text style={{ maxWidth: '600px' }} text="analytics.sales.enableDescription" />
48
+ <Button {...enableAction} variant="cta" />
49
+ {error && <Text variant="small">{error}</Text>}
50
+ </View>
51
+ </Page>
52
+ )}
53
+ </View>
54
+ )
55
+ }
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 analytics for website traffic and top performing pages.',
4
+ description: 'Workspace insight KPIs for members, resources, and traffic.',
5
5
  icon: 'chart',
6
6
  status: ['beta'],
7
7
  }
@@ -0,0 +1,93 @@
1
+ import React from 'react'
2
+ import { Button, View, PageSection, Text } from '@ossy/design-system'
3
+
4
+ const Cover = ({
5
+ title,
6
+ titleMaxWidth = '1100px',
7
+ text,
8
+ actions = [],
9
+ }) => (
10
+ <View
11
+ gap="l"
12
+ placeContent="center"
13
+ layout="column"
14
+ justifyContent="center"
15
+ style={{
16
+ height: '100%',
17
+ borderRadius: 'var(--space-m)',
18
+ padding: 'var(--space-xl) var(--space-s)',
19
+ }}
20
+ >
21
+ <View gap="m" alignItems="center">
22
+ <Text as="h1" variant="display" style={{ maxWidth: titleMaxWidth, textWrap: 'balance' }}>
23
+ {title}
24
+ </Text>
25
+ <Text as="h2" variant="heading-tertiary" style={{ maxWidth: '800px' }}>
26
+ {text}
27
+ </Text>
28
+ </View>
29
+ <View gap="m" layout="row" justifyContent="center" style={{ flexWrap: 'wrap' }}>
30
+ {actions.map(({ label, ...props }) => (
31
+ <Button {...props} key={label}>
32
+ {label}
33
+ </Button>
34
+ ))}
35
+ </View>
36
+ </View>
37
+ )
38
+
39
+ export function HeroCover ({
40
+ title,
41
+ titleMaxWidth = '1100px',
42
+ text,
43
+ actions = [],
44
+ id = 'analytics-hero-cover',
45
+ surfaceAs = 'div',
46
+ maxWidth = 'l',
47
+ surface = 'hero',
48
+ ...props
49
+ }) {
50
+ const { style: passthroughStyle, ...pageSectionProps } = props
51
+
52
+ return (
53
+ <PageSection
54
+ id={id}
55
+ surfaceAs={surfaceAs}
56
+ maxWidth={maxWidth}
57
+ surface={surface}
58
+ {...pageSectionProps}
59
+ data-rounded
60
+ data-component="@ossy/analytics/cover"
61
+ style={{
62
+ backgroundAttachment: 'scroll',
63
+ boxShadow: 'inset 0 1px 0 color-mix(in srgb, var(--foreground) 6%, transparent)',
64
+ ...passthroughStyle,
65
+ }}
66
+ >
67
+ <style href="@ossy/analytics/cover" precedence="high">
68
+ {`
69
+ [data-component="@ossy/analytics/cover"] {
70
+ gap: var(--space-m);
71
+ min-height: min(42vh, 420px);
72
+ height: auto;
73
+ }
74
+ [data-rounded] {
75
+ border-radius: var(--space-m);
76
+ overflow: hidden;
77
+ }
78
+ @media (min-width: 900px) {
79
+ [data-component="@ossy/analytics/cover"] {
80
+ min-height: min(46vh, 520px);
81
+ }
82
+ }
83
+ `}
84
+ </style>
85
+ <Cover
86
+ title={title}
87
+ titleMaxWidth={titleMaxWidth}
88
+ text={text}
89
+ actions={actions}
90
+ />
91
+ </PageSection>
92
+ )
93
+ }
@@ -0,0 +1,69 @@
1
+ import React from 'react'
2
+ import { Text, View, Icon, PageSection, useLocale } from '@ossy/design-system'
3
+ import { HeroCover } from './HeroCover.jsx'
4
+
5
+ export default function SalesSection ({ cover, features }) {
6
+ const { t } = useLocale()
7
+
8
+ return (
9
+ <PageSection maxWidth="xl" gap="l">
10
+ <View
11
+ gap="l"
12
+ surface="primary"
13
+ style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
14
+ >
15
+ <HeroCover {...cover} />
16
+
17
+ <View gap="m" inset="s">
18
+ <Text variant="heading-secondary" as="h2">
19
+ {t('analytics.sales.overview')}
20
+ </Text>
21
+ <View gap="m" layout="row-wrap">
22
+ {features.map((x) => (
23
+ <View
24
+ key={x.title}
25
+ roundness="m"
26
+ surface="primary"
27
+ inset="m"
28
+ gap="m"
29
+ style={{
30
+ width: 200,
31
+ border: '1px solid var(--separator)',
32
+ }}
33
+ >
34
+ <View justifyContent="center" alignItems="center">
35
+ <Icon name={x.icon} size="m" />
36
+ </View>
37
+ <Text variant="small" style={{ fontWeight: 'bold', textAlign: 'center' }}>{x.title}</Text>
38
+ <Text variant="small" style={{ textAlign: 'center' }}>{x.text}</Text>
39
+ </View>
40
+ ))}
41
+ </View>
42
+ </View>
43
+
44
+ <View gap="m" inset="s">
45
+ <Text variant="heading-secondary" as="h2">
46
+ {t('analytics.sales.features')}
47
+ </Text>
48
+ {features.map((x) => (
49
+ <View
50
+ key={x.title}
51
+ roundness="m"
52
+ gap="m"
53
+ inset="l"
54
+ style={{ border: '1px solid var(--separator)' }}
55
+ >
56
+ <View layout="row" gap="s" alignItems="center">
57
+ <View justifyContent="center" alignItems="center">
58
+ <Icon name={x.icon} size="m" />
59
+ </View>
60
+ <Text variant="heading-tertiary" as="h3">{x.title}</Text>
61
+ </View>
62
+ <Text>{x.text}</Text>
63
+ </View>
64
+ ))}
65
+ </View>
66
+ </View>
67
+ </PageSection>
68
+ )
69
+ }
@@ -0,0 +1,51 @@
1
+ import { useLocale } from '@ossy/design-system'
2
+ import { useRouter } from '@ossy/router-react'
3
+ import { OpenSignUp } from '@ossy/authentication'
4
+
5
+ const ANALYTICS_SERVICE = '@ossy/analytics'
6
+
7
+ export function useAnalyticsHomeContent () {
8
+ const { t } = useLocale()
9
+ const router = useRouter()
10
+
11
+ const analyticsHome = router.getHref('analytics/home') || '/analytics'
12
+ const signUpHref = (() => {
13
+ const base = router.getHref('sign-up') || router.getHref('@sign-up') || '/sign-up'
14
+ const url = new URL(base, 'http://local.invalid')
15
+ url.searchParams.set('redirect', analyticsHome)
16
+ return `${url.pathname}${url.search}`
17
+ })()
18
+
19
+ const cover = {
20
+ title: t('analytics.home.cover.title'),
21
+ text: t('analytics.home.cover.text'),
22
+ actions: [
23
+ {
24
+ ...OpenSignUp,
25
+ variant: 'cta',
26
+ href: signUpHref,
27
+ label: t('analytics.home.cover.ctaPrimary'),
28
+ },
29
+ ],
30
+ }
31
+
32
+ const features = [
33
+ {
34
+ title: t('analytics.home.features.members.title'),
35
+ icon: 'user',
36
+ text: t('analytics.home.features.members.text'),
37
+ },
38
+ {
39
+ title: t('analytics.home.features.resources.title'),
40
+ icon: 'folder',
41
+ text: t('analytics.home.features.resources.text'),
42
+ },
43
+ {
44
+ title: t('analytics.home.features.traffic.title'),
45
+ icon: 'chart',
46
+ text: t('analytics.home.features.traffic.text'),
47
+ },
48
+ ]
49
+
50
+ return { cover, features, ANALYTICS_SERVICE }
51
+ }
@@ -1,8 +1,10 @@
1
- import React, { useMemo } from 'react'
2
- import { Text, View, Page, useLocale } from '@ossy/design-system'
3
- import { usePageViewStats } from './usePageViewStats.js'
4
- import { useSdk } from '@ossy/sdk-react'
1
+ import React from 'react'
2
+ import { useApp } from '@ossy/app/shell'
5
3
  import { GetWorkspace } from '@ossy/workspaces'
4
+ import { isServiceEntitled } from '@ossy/workspaces/entitlements'
5
+ import { useSdk } from '@ossy/sdk-react'
6
+ import AnalyticsSalesPage from './AnalyticsSalesPage.jsx'
7
+ import AnalyticsProductHome from './AnalyticsProductHome.jsx'
6
8
 
7
9
  export const metadata = {
8
10
  id: 'analytics/home',
@@ -12,100 +14,20 @@ export const metadata = {
12
14
  },
13
15
  }
14
16
 
15
- function SummaryCard({ label, value }) {
16
- return (
17
- <View
18
- surface="primary"
19
- roundness="m"
20
- inset="m"
21
- style={{ border: '1px solid var(--separator)' }}
22
- >
23
- <Text variant="small" style={{ opacity: 0.75 }}>{label}</Text>
24
- <Text variant="heading-secondary" as="h2">{value}</Text>
25
- </View>
26
- )
27
- }
28
-
29
- export default function AnalyticsPage() {
30
- const { t } = useLocale()
17
+ /**
18
+ * Analytics home — product KPIs only when signed in and entitled.
19
+ * Otherwise show the sales page (no KPI / page-view loads).
20
+ */
21
+ export default function AnalyticsPage () {
22
+ const app = useApp()
31
23
  const sdk = useSdk()
32
- const { data: workspace } = sdk.read(GetWorkspace)
33
- const { from, to } = useMemo(() => {
34
- const now = Date.now()
35
- return { from: now - 30 * 24 * 60 * 60 * 1000, to: now }
36
- }, [])
37
-
38
- const { status, totalViews, dailyViews, topPaths } = usePageViewStats({
39
- workspaceId: workspace?.id,
40
- from,
41
- to,
42
- limit: 10,
43
- })
44
-
45
- return (
46
- <Page
47
- title="analytics.home.title"
48
- description="analytics.home.description"
49
- >
50
- <View gap="m">
51
- {status === 'loading' && (
52
- <View surface="primary" roundness="m" inset="m">
53
- <Text>{t('analytics.home.loading')}</Text>
54
- </View>
55
- )}
56
-
57
- {status === 'error' && (
58
- <View surface="primary" roundness="m" inset="m">
59
- <Text>{t('analytics.home.error')}</Text>
60
- </View>
61
- )}
62
-
63
- {status === 'success' && (
64
- <>
65
- <View layout="row" gap="m">
66
- <SummaryCard label={t('analytics.home.totalPageViews')} value={`${totalViews}`} />
67
- <SummaryCard label={t('analytics.home.trackedPages')} value={`${topPaths.length}`} />
68
- <SummaryCard label={t('analytics.home.daysWithTraffic')} value={`${dailyViews.length}`} />
69
- </View>
24
+ const isAuthenticated = !!app?.isAuthenticated
25
+ const { data: workspace } = sdk.read(GetWorkspace, undefined, { enabled: isAuthenticated })
26
+ const entitled = isServiceEntitled(workspace?.services, '@ossy/analytics')
70
27
 
71
- <View layout="row" gap="m" style={{ alignItems: 'flex-start' }}>
72
- <View
73
- surface="primary"
74
- roundness="m"
75
- inset="m"
76
- gap="s"
77
- style={{ border: '1px solid var(--separator)', flex: 1 }}
78
- >
79
- <Text variant="heading-secondary" as="h2" text="analytics.home.topPaths" />
80
- {topPaths.length === 0 && <Text variant="small" text="analytics.home.noPageViews" />}
81
- {topPaths.map((item) => (
82
- <View key={item.path || 'unknown'} layout="row" justifyContent="space-between">
83
- <Text variant="small">{item.path || t('analytics.home.unknownPath')}</Text>
84
- <Text variant="small">{item.views}</Text>
85
- </View>
86
- ))}
87
- </View>
28
+ if (isAuthenticated && entitled) {
29
+ return <AnalyticsProductHome />
30
+ }
88
31
 
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.dailyPageViews" />
97
- {dailyViews.length === 0 && <Text variant="small" text="analytics.home.noDailyData" />}
98
- {dailyViews.map((item) => (
99
- <View key={item.date} layout="row" justifyContent="space-between">
100
- <Text variant="small">{item.date}</Text>
101
- <Text variant="small">{item.views}</Text>
102
- </View>
103
- ))}
104
- </View>
105
- </View>
106
- </>
107
- )}
108
- </View>
109
- </Page>
110
- )
32
+ return <AnalyticsSalesPage isAuthenticated={isAuthenticated} />
111
33
  }
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "analytics/home.documentTitle": "Analytics",
3
3
  "analytics.home.title": "Analytics",
4
- "analytics.home.description": "Workspace analytics for website traffic and top performing pages.",
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",
@@ -11,5 +14,19 @@
11
14
  "analytics.home.noPageViews": "No page views yet.",
12
15
  "analytics.home.unknownPath": "(unknown path)",
13
16
  "analytics.home.dailyPageViews": "Daily page views",
14
- "analytics.home.noDailyData": "No daily data yet."
17
+ "analytics.home.noDailyData": "No daily data yet.",
18
+ "analytics.home.cover.title": "See how your workspace is used",
19
+ "analytics.home.cover.text": "Members, resources, and page traffic — insight KPIs for the teams building on the platform.",
20
+ "analytics.home.cover.ctaPrimary": "Get started free",
21
+ "analytics.home.features.members.title": "Members",
22
+ "analytics.home.features.members.text": "Track how many people are in the workspace.",
23
+ "analytics.home.features.resources.title": "Resources",
24
+ "analytics.home.features.resources.text": "See how much content and data you store.",
25
+ "analytics.home.features.traffic.title": "Traffic",
26
+ "analytics.home.features.traffic.text": "Page views over the last 30 days, by path and by day.",
27
+ "analytics.sales.overview": "Overview",
28
+ "analytics.sales.features": "Features",
29
+ "analytics.sales.enable": "Enable analytics",
30
+ "analytics.sales.enableDescription": "Activate analytics for your workspace to view member, resource, and traffic KPIs.",
31
+ "analytics.sales.enableError": "Could not enable analytics. Try again or contact support."
15
32
  }
@@ -0,0 +1,4 @@
1
+ export const metadata = {
2
+ id: '@ossy/analytics/actions/get-workspace-kpis',
3
+ access: 'workspace',
4
+ }
@@ -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
@@ -1 +1,2 @@
1
1
  export { Definition } from './Definition.js'
2
+ export { metadata as GetWorkspaceKpis } from './get-workspace-kpis.action.js'
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "analytics/home.documentTitle": "Analys",
3
3
  "analytics.home.title": "Analys",
4
- "analytics.home.description": "Workspace-analys för webbtrafik och mest besökta sidor.",
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",
@@ -11,5 +14,19 @@
11
14
  "analytics.home.noPageViews": "Inga sidvisningar ännu.",
12
15
  "analytics.home.unknownPath": "(okänd sökväg)",
13
16
  "analytics.home.dailyPageViews": "Dagliga sidvisningar",
14
- "analytics.home.noDailyData": "Ingen daglig data ännu."
17
+ "analytics.home.noDailyData": "Ingen daglig data ännu.",
18
+ "analytics.home.cover.title": "Se hur workspace används",
19
+ "analytics.home.cover.text": "Medlemmar, resurser och sidtrafik — KPIer för team som bygger på plattformen.",
20
+ "analytics.home.cover.ctaPrimary": "Kom igång gratis",
21
+ "analytics.home.features.members.title": "Medlemmar",
22
+ "analytics.home.features.members.text": "Se hur många personer som finns i workspace.",
23
+ "analytics.home.features.resources.title": "Resurser",
24
+ "analytics.home.features.resources.text": "Följ hur mycket innehåll och data ni lagrar.",
25
+ "analytics.home.features.traffic.title": "Trafik",
26
+ "analytics.home.features.traffic.text": "Sidvisningar de senaste 30 dagarna, per sökväg och dag.",
27
+ "analytics.sales.overview": "Översikt",
28
+ "analytics.sales.features": "Funktioner",
29
+ "analytics.sales.enable": "Aktivera analys",
30
+ "analytics.sales.enableDescription": "Aktivera analys för workspace för att se KPIer för medlemmar, resurser och trafik.",
31
+ "analytics.sales.enableError": "Kunde inte aktivera analys. Försök igen eller kontakta support."
15
32
  }
@@ -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
+ }