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:
Deluan Quintão
2024-12-17 17:10:55 -05:00
committed by GitHub
parent bc3576e092
commit 8e2052ff95
26 changed files with 665 additions and 39 deletions
+67
View File
@@ -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 lines 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()