mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 20:47:35 +03:00
feat(Insights): add anonymous usage data collection (#3543)
* feat(insights): initial code (WIP) * feat(insights): add more info * feat(insights): add fs info * feat(insights): export insights.Data Signed-off-by: Deluan <deluan@navidrome.org> * feat(insights): more config info Signed-off-by: Deluan <deluan@navidrome.org> * refactor(insights): move data struct to its own package Signed-off-by: Deluan <deluan@navidrome.org> * refactor(insights): omit some attrs if empty Signed-off-by: Deluan <deluan@navidrome.org> * feat(insights): send insights to server, add option to disable Signed-off-by: Deluan <deluan@navidrome.org> * fix(insights): remove info about anonymous login Signed-off-by: Deluan <deluan@navidrome.org> * chore(insights): fix lint Signed-off-by: Deluan <deluan@navidrome.org> * fix(insights): disable collector if EnableExternalServices is false Signed-off-by: Deluan <deluan@navidrome.org> * fix(insights): fix type casting for 32bit platforms Signed-off-by: Deluan <deluan@navidrome.org> * fix(insights): remove EnableExternalServices from the collection (as it will always be false) Signed-off-by: Deluan <deluan@navidrome.org> * chore(insights): fix lint Signed-off-by: Deluan <deluan@navidrome.org> * refactor(insights): rename function for consistency Signed-off-by: Deluan <deluan@navidrome.org> * feat(insights): log the data sent to the collector server Signed-off-by: Deluan <deluan@navidrome.org> * feat(insights): add last collection timestamp to the "about" dialog. Also add opt-out info to the SignUp form Signed-off-by: Deluan <deluan@navidrome.org> * feat(insights): only sends the initial data collection after an admin user is created Signed-off-by: Deluan <deluan@navidrome.org> * feat(insights): remove dangling comment Signed-off-by: Deluan <deluan@navidrome.org> * feat(insights): Translate insights messages Signed-off-by: Deluan <deluan@navidrome.org> * fix(insights): reporting empty library Signed-off-by: Deluan <deluan@navidrome.org> * refactor: move URL to consts.js Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
bc3576e092
commit
8e2052ff95
26 changed files with 665 additions and 39 deletions
|
@ -123,6 +123,7 @@ const Admin = (props) => {
|
|||
<Resource name="genre" />,
|
||||
<Resource name="playlistTrack" />,
|
||||
<Resource name="keepalive" />,
|
||||
<Resource name="insights" />,
|
||||
<Player />,
|
||||
]}
|
||||
</RAAdmin>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
export const REST_URL = '/api'
|
||||
|
||||
export const INSIGHTS_DOC_URL =
|
||||
'https://navidrome.org/docs/getting-started/insights'
|
||||
|
||||
export const M3U_MIME_TYPE = 'audio/x-mpegurl'
|
||||
|
||||
export const AUTO_THEME_ID = 'AUTO_THEME_ID'
|
||||
|
|
|
@ -11,10 +11,11 @@ import TableCell from '@material-ui/core/TableCell'
|
|||
import Paper from '@material-ui/core/Paper'
|
||||
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
|
||||
import inflection from 'inflection'
|
||||
import { useTranslate } from 'react-admin'
|
||||
import { useGetOne, usePermissions, useTranslate } from 'react-admin'
|
||||
import config from '../config'
|
||||
import { DialogTitle } from './DialogTitle'
|
||||
import { DialogContent } from './DialogContent'
|
||||
import { INSIGHTS_DOC_URL } from '../consts.js'
|
||||
|
||||
const links = {
|
||||
homepage: 'navidrome.org',
|
||||
|
@ -51,6 +52,9 @@ const LinkToVersion = ({ version }) => {
|
|||
|
||||
const AboutDialog = ({ open, onClose }) => {
|
||||
const translate = useTranslate()
|
||||
const { permissions } = usePermissions()
|
||||
const { data, loading } = useGetOne('insights', 'insights_status')
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} aria-labelledby="about-dialog-title" open={open}>
|
||||
<DialogTitle id="about-dialog-title" onClose={onClose}>
|
||||
|
@ -87,6 +91,18 @@ const AboutDialog = ({ open, onClose }) => {
|
|||
</TableRow>
|
||||
)
|
||||
})}
|
||||
{permissions === 'admin' ? (
|
||||
<TableRow>
|
||||
<TableCell align="right" component="th" scope="row">
|
||||
{translate(`about.links.lastInsightsCollection`)}:
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
<Link href={INSIGHTS_DOC_URL}>
|
||||
{(!loading && data?.lastRun) || 'N/A'}{' '}
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
<TableRow>
|
||||
<TableCell align="right" component="th" scope="row">
|
||||
<Link
|
||||
|
|
|
@ -214,7 +214,8 @@
|
|||
"password": "Password",
|
||||
"sign_in": "Sign in",
|
||||
"sign_in_error": "Authentication failed, please retry",
|
||||
"logout": "Logout"
|
||||
"logout": "Logout",
|
||||
"insightsCollectionNote": "Navidrome collects anonymous usage data to\nhelp improve the project. Click [here] to learn\nmore and to opt-out if you want"
|
||||
},
|
||||
"validation": {
|
||||
"invalidChars": "Please only use letters and numbers",
|
||||
|
@ -435,7 +436,8 @@
|
|||
"links": {
|
||||
"homepage": "Home page",
|
||||
"source": "Source code",
|
||||
"featureRequests": "Feature requests"
|
||||
"featureRequests": "Feature requests",
|
||||
"lastInsightsCollection": "Last insights collection"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
|
|
|
@ -6,6 +6,7 @@ import Button from '@material-ui/core/Button'
|
|||
import Card from '@material-ui/core/Card'
|
||||
import CardActions from '@material-ui/core/CardActions'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import Link from '@material-ui/core/Link'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import { ThemeProvider, makeStyles } from '@material-ui/core/styles'
|
||||
import {
|
||||
|
@ -24,6 +25,7 @@ import useCurrentTheme from '../themes/useCurrentTheme'
|
|||
import config from '../config'
|
||||
import { clearQueue } from '../actions'
|
||||
import { retrieveTranslation } from '../i18n'
|
||||
import { INSIGHTS_DOC_URL } from '../consts.js'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme) => ({
|
||||
|
@ -81,6 +83,13 @@ const useStyles = makeStyles(
|
|||
systemNameLink: {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
message: {
|
||||
marginTop: '1em',
|
||||
padding: '0 1em 1em 1em',
|
||||
textAlign: 'center',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: '0.875em',
|
||||
},
|
||||
}),
|
||||
{ name: 'NDLogin' },
|
||||
)
|
||||
|
@ -173,6 +182,62 @@ const FormLogin = ({ loading, handleSubmit, validate }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const InsightsNotice = ({ url }) => {
|
||||
const translate = useTranslate()
|
||||
const classes = useStyles()
|
||||
|
||||
const anchorRegex = /\[(.+?)]/g
|
||||
const originalMsg = translate('ra.auth.insightsCollectionNote')
|
||||
|
||||
// Split the entire message on newlines
|
||||
const lines = originalMsg.split('\n')
|
||||
|
||||
const renderedLines = lines.map((line, lineIndex) => {
|
||||
const segments = []
|
||||
let lastIndex = 0
|
||||
let match
|
||||
|
||||
// Find bracketed text in each line
|
||||
while ((match = anchorRegex.exec(line)) !== null) {
|
||||
// match.index is where "[something]" starts
|
||||
// match[1] is the text inside the brackets
|
||||
const bracketText = match[1]
|
||||
|
||||
// Push the text before the bracket
|
||||
segments.push(line.slice(lastIndex, match.index))
|
||||
|
||||
// Push the <Link> component
|
||||
segments.push(
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key={`${lineIndex}-${match.index}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{bracketText}
|
||||
</Link>,
|
||||
)
|
||||
|
||||
// Update lastIndex to the character right after the bracketed text
|
||||
lastIndex = match.index + match[0].length
|
||||
}
|
||||
|
||||
// Push the remaining text after the last bracket
|
||||
segments.push(line.slice(lastIndex))
|
||||
|
||||
// Return this line’s parts, plus a <br/> if not the last line
|
||||
return (
|
||||
<React.Fragment key={lineIndex}>
|
||||
{segments}
|
||||
{lineIndex < lines.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
|
||||
return <div className={classes.message}>{renderedLines}</div>
|
||||
}
|
||||
|
||||
const FormSignUp = ({ loading, handleSubmit, validate }) => {
|
||||
const translate = useTranslate()
|
||||
const classes = useStyles()
|
||||
|
@ -237,6 +302,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => {
|
|||
{translate('ra.auth.buttonCreateAdmin')}
|
||||
</Button>
|
||||
</CardActions>
|
||||
<InsightsNotice url={INSIGHTS_DOC_URL} />
|
||||
</Card>
|
||||
<Notification />
|
||||
</div>
|
||||
|
@ -245,6 +311,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => {
|
|||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Login = ({ location }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const translate = useTranslate()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue