From f7c39eeebb5fcc17222b62972a1079c70bc0a89d Mon Sep 17 00:00:00 2001 From: David Ralph Date: Mon, 21 Jun 2021 17:42:14 +0100 Subject: [PATCH] feat: add opt-in umami analytics (WIP) --- src/App.jsx | 2 + src/components/modals/ErrorBoundary.jsx | 1 + src/components/modals/Modals.jsx | 19 +++++-- .../modals/main/marketplace/Lightbox.jsx | 2 + .../main/marketplace/sections/Added.jsx | 11 ++++- .../main/marketplace/sections/Marketplace.jsx | 19 +++++-- .../main/marketplace/sections/Sideload.jsx | 1 + .../modals/main/settings/Checkbox.jsx | 2 + .../modals/main/settings/Dropdown.jsx | 2 + src/components/modals/main/settings/Radio.jsx | 2 + .../modals/main/settings/Slider.jsx | 2 + .../modals/main/settings/Switch.jsx | 2 + .../modals/main/tabs/backend/Tabs.jsx | 1 + .../widgets/background/Favourite.jsx | 2 + .../widgets/background/Maximise.jsx | 2 + .../widgets/background/PhotoInformation.jsx | 1 + src/components/widgets/navbar/Notes.jsx | 2 + .../widgets/quicklinks/QuickLinks.jsx | 4 ++ src/components/widgets/quote/Quote.jsx | 4 ++ src/components/widgets/search/Search.jsx | 2 + src/index.js | 11 +++++ src/modules/constants.js | 2 + src/modules/helpers/analytics.js | 49 +++++++++++++++++++ 23 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 src/modules/helpers/analytics.js diff --git a/src/App.jsx b/src/App.jsx index e64cc01d..9acf9f35 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -30,6 +30,8 @@ export default class App extends React.PureComponent { SettingsFunctions.loadSettings(true); } }); + + window.analytics.tabLoad(); } render() { diff --git a/src/components/modals/ErrorBoundary.jsx b/src/components/modals/ErrorBoundary.jsx index fdb25762..aa00d85c 100644 --- a/src/components/modals/ErrorBoundary.jsx +++ b/src/components/modals/ErrorBoundary.jsx @@ -13,6 +13,7 @@ export default class ErrorBoundary extends React.PureComponent { static getDerivedStateFromError(error) { console.log(error); + window.analytics.postEvent('modalUpdate', 'Error occurred'); return { error: true }; diff --git a/src/components/modals/Modals.jsx b/src/components/modals/Modals.jsx index d0721f1f..00f76f99 100644 --- a/src/components/modals/Modals.jsx +++ b/src/components/modals/Modals.jsx @@ -27,6 +27,7 @@ export default class Modals extends React.PureComponent { this.setState({ welcomeModal: true }); + window.analytics.postEvent('modalUpdate', 'Opened welcome modal'); } // hide refresh reminder once the user has refreshed the page @@ -38,21 +39,29 @@ export default class Modals extends React.PureComponent { this.setState({ welcomeModal: false }); + window.analytics.postEvent('modalUpdate', 'Closed welcome modal'); + } + + toggleModal(type, action) { + this.setState({ + [type]: action + }); + window.analytics.postEvent('modalUpdate', `${(action === false) ? 'Closed' : 'Opened'} ${type.replace('Modal', '')} modal`); } render() { return ( <> - this.setState({ [modal]: true })}/> - this.setState({ mainModal: false })} isOpen={this.state.mainModal} className='Modal mainModal' overlayClassName='Overlay' ariaHideApp={false}> -
this.setState({ mainModal: false })}/> + this.toggleModal(modal, true)}/> + this.toggleModal('mainModal', false)} isOpen={this.state.mainModal} className='Modal mainModal' overlayClassName='Overlay' ariaHideApp={false}> +
this.toggleModal('mainModal', false)}/> this.closeWelcome()} isOpen={this.state.welcomeModal} className='Modal welcomemodal mainModal' overlayClassName='Overlay' ariaHideApp={false}> this.closeWelcome()}/> - this.setState({ feedbackModal: false })} isOpen={this.state.feedbackModal} className='Modal mainModal' overlayClassName='Overlay' ariaHideApp={false}> - this.setState({ feedbackModal: false })}/> + this.toggleModal('feedbackModal', false)} isOpen={this.state.feedbackModal} className='Modal mainModal' overlayClassName='Overlay' ariaHideApp={false}> + this.toggleModal('feedbackModal', false)}/> diff --git a/src/components/modals/main/marketplace/Lightbox.jsx b/src/components/modals/main/marketplace/Lightbox.jsx index 2b685158..3baee097 100644 --- a/src/components/modals/main/marketplace/Lightbox.jsx +++ b/src/components/modals/main/marketplace/Lightbox.jsx @@ -1,4 +1,6 @@ export default function Lightbox(props) { + window.analytics.postEvent('modalUpdate', 'Lightbox used'); + return ( <> × diff --git a/src/components/modals/main/marketplace/sections/Added.jsx b/src/components/modals/main/marketplace/sections/Added.jsx index 7560d744..fe8501ea 100644 --- a/src/components/modals/main/marketplace/sections/Added.jsx +++ b/src/components/modals/main/marketplace/sections/Added.jsx @@ -42,6 +42,7 @@ export default class Added extends React.PureComponent { }, button: this.buttons.uninstall }); + window.analytics.postEvent('marketplaceUpdate', `Item viewed`); } else { this.setState({ item: {} @@ -58,9 +59,11 @@ export default class Added extends React.PureComponent { button: '', installed: JSON.parse(localStorage.getItem('installed')) }); + + window.analytics.postEvent('marketplaceUpdate', 'Uninstall used'); } - sortAddons(value) { + sortAddons(value, sendEvent) { let installed = JSON.parse(localStorage.getItem('installed')); switch (value) { case 'newest': @@ -82,10 +85,14 @@ export default class Added extends React.PureComponent { this.setState({ installed: installed }); + + if (sendEvent) { + window.analytics.postEvent('marketplaceUpdate', 'Sort used'); + } } componentDidMount() { - this.sortAddons(localStorage.getItem('sortAddons')); + this.sortAddons(localStorage.getItem('sortAddons'), false); } render() { diff --git a/src/components/modals/main/marketplace/sections/Marketplace.jsx b/src/components/modals/main/marketplace/sections/Marketplace.jsx index a2e55a06..17268d59 100644 --- a/src/components/modals/main/marketplace/sections/Marketplace.jsx +++ b/src/components/modals/main/marketplace/sections/Marketplace.jsx @@ -67,6 +67,8 @@ export default class Marketplace extends React.PureComponent { }, button: button }); + + window.analytics.postEvent('marketplaceItemUpdate', `${this.state.item.display_name} viewed`); } else { this.setState({ item: {} @@ -89,7 +91,7 @@ export default class Marketplace extends React.PureComponent { done: true }); - this.sortMarketplace(localStorage.getItem('sortMarketplace')); + this.sortMarketplace(localStorage.getItem('sortMarketplace'), false); } manage(type) { @@ -103,9 +105,12 @@ export default class Marketplace extends React.PureComponent { this.setState({ button: (type === 'install') ? this.buttons.uninstall : this.buttons.install }); + + window.analytics.postEvent('marketplaceItemUpdate', `${this.state.item.display_name} ${(type === 'install' ? 'installed': 'uninstalled')}`); + window.analytics.postEvent('marketplaceUpdate', `${(type === 'install' ? 'Install': 'Uninstall')} used`); } - sortMarketplace(value) { + sortMarketplace(value, sendEvent) { let items = this.state.oldItems; switch (value) { case 'a-z': @@ -127,6 +132,10 @@ export default class Marketplace extends React.PureComponent { items: items, sortType: value }); + + if (sendEvent) { + window.analytics.postEvent('marketplaceUpdate', 'Sort used'); + } } componentDidMount() { @@ -154,11 +163,15 @@ export default class Marketplace extends React.PureComponent { }; const featured = () => { + const openFeatured = () => { + window.analytics.postEvent('marketplaceUpdate', 'Featured click used'); + window.open(this.state.featured.buttonLink); + } return (

{this.state.featured.title}

{this.state.featured.name}

- +
); } diff --git a/src/components/modals/main/marketplace/sections/Sideload.jsx b/src/components/modals/main/marketplace/sections/Sideload.jsx index db18b9d6..14b7eb17 100644 --- a/src/components/modals/main/marketplace/sections/Sideload.jsx +++ b/src/components/modals/main/marketplace/sections/Sideload.jsx @@ -10,6 +10,7 @@ export default function Sideload() { const install = (input) => { MarketplaceFunctions.install(input.type, input); toast(window.language.toasts.installed); + window.analytics.postEvent('marketplaceUpdate', 'Sideload used'); }; return ( diff --git a/src/components/modals/main/settings/Checkbox.jsx b/src/components/modals/main/settings/Checkbox.jsx index 07086a0d..1315f76f 100644 --- a/src/components/modals/main/settings/Checkbox.jsx +++ b/src/components/modals/main/settings/Checkbox.jsx @@ -21,6 +21,8 @@ export default class Checkbox extends React.PureComponent { checked: (this.state.checked === true) ? false : true }); + window.analytics.postEvent('settingUpdate', `${(this.state.checked === true) ? 'Enabled' : 'Disabled'} setting ${this.props.name}`); + if (this.props.element) { if (!document.querySelector(this.props.element)) { document.querySelector('.reminder-info').style.display = 'block'; diff --git a/src/components/modals/main/settings/Dropdown.jsx b/src/components/modals/main/settings/Dropdown.jsx index a3b87866..d4146a7b 100644 --- a/src/components/modals/main/settings/Dropdown.jsx +++ b/src/components/modals/main/settings/Dropdown.jsx @@ -22,6 +22,8 @@ export default class Dropdown extends React.PureComponent { return; } + window.analytics.postEvent('settingUpdate', `Changed setting ${this.props.name} from ${this.state.value} to ${value}`); + this.setState({ value: value, title: e.target[e.target.selectedIndex].text diff --git a/src/components/modals/main/settings/Radio.jsx b/src/components/modals/main/settings/Radio.jsx index 05591e63..772221dc 100644 --- a/src/components/modals/main/settings/Radio.jsx +++ b/src/components/modals/main/settings/Radio.jsx @@ -29,6 +29,8 @@ export default class Radio extends React.PureComponent { value: value }); + window.analytics.postEvent('settingUpdate', `Changed setting ${this.props.name} from ${this.state.value} to ${value}`); + if (this.props.element) { if (!document.querySelector(this.props.element)) { document.querySelector('.reminder-info').style.display = 'block'; diff --git a/src/components/modals/main/settings/Slider.jsx b/src/components/modals/main/settings/Slider.jsx index b6726477..188e0105 100644 --- a/src/components/modals/main/settings/Slider.jsx +++ b/src/components/modals/main/settings/Slider.jsx @@ -19,6 +19,8 @@ export default class Slider extends React.PureComponent { handleChange = (e, text) => { let { value } = e.target; + window.analytics.postEvent('settingUpdate', `Changed setting ${this.props.name} from ${this.state.value} to ${value}`); + if (text) { if (value === '') { return this.setState({ diff --git a/src/components/modals/main/settings/Switch.jsx b/src/components/modals/main/settings/Switch.jsx index 290c41c5..96503c41 100644 --- a/src/components/modals/main/settings/Switch.jsx +++ b/src/components/modals/main/settings/Switch.jsx @@ -21,6 +21,8 @@ export default class Switch extends React.PureComponent { checked: (this.state.checked === true) ? false : true }); + window.analytics.postEvent('settingUpdate', `${(this.state.checked === true) ? 'Enabled' : 'Disabled'} setting ${this.props.name}`); + if (this.props.element) { if (!document.querySelector(this.props.element)) { document.querySelector('.reminder-info').style.display = 'block'; diff --git a/src/components/modals/main/tabs/backend/Tabs.jsx b/src/components/modals/main/tabs/backend/Tabs.jsx index 4d503fb8..f042a565 100644 --- a/src/components/modals/main/tabs/backend/Tabs.jsx +++ b/src/components/modals/main/tabs/backend/Tabs.jsx @@ -13,6 +13,7 @@ export default class Tabs extends React.PureComponent { } onClick = (tab) => { + window.analytics.postEvent('tabUpdate', `Changed tab from ${this.state.currentTab} to ${tab}`); this.setState({ currentTab: tab }); diff --git a/src/components/widgets/background/Favourite.jsx b/src/components/widgets/background/Favourite.jsx index 76747155..f61cafce 100644 --- a/src/components/widgets/background/Favourite.jsx +++ b/src/components/widgets/background/Favourite.jsx @@ -19,6 +19,7 @@ export default class Favourite extends React.PureComponent { this.setState({ favourited: }); + window.analytics.postEvent('featureUpdate', 'Feature background favourite used'); } else { const url = document.getElementById('backgroundImage').style.backgroundImage.replace('url("', '').replace('")', ''); @@ -37,6 +38,7 @@ export default class Favourite extends React.PureComponent { this.setState({ favourited: }); + window.analytics.postEvent('featureUpdate', 'Feature background unfavourite used'); } } diff --git a/src/components/widgets/background/Maximise.jsx b/src/components/widgets/background/Maximise.jsx index ea39ff8d..b5ff9855 100644 --- a/src/components/widgets/background/Maximise.jsx +++ b/src/components/widgets/background/Maximise.jsx @@ -44,12 +44,14 @@ export default class Maximise extends React.PureComponent { }); this.setAttribute(0, 100); + window.analytics.postEvent('featureUpdate', 'Feature background maximise used'); } else { this.setState({ hidden: false }); this.setAttribute(localStorage.getItem('blur'), localStorage.getItem('brightness'), true); + window.analytics.postEvent('featureUpdate', 'Feature background unmaximise used'); } } diff --git a/src/components/widgets/background/PhotoInformation.jsx b/src/components/widgets/background/PhotoInformation.jsx index 4609882c..fb4d091f 100644 --- a/src/components/widgets/background/PhotoInformation.jsx +++ b/src/components/widgets/background/PhotoInformation.jsx @@ -18,6 +18,7 @@ const downloadImage = async (info) => { document.body.appendChild(link); link.click(); document.body.removeChild(link); + window.analytics.postEvent('featureUpdate', 'Feature background download used'); }; export default function PhotoInformation(props) { diff --git a/src/components/widgets/navbar/Notes.jsx b/src/components/widgets/navbar/Notes.jsx index d325f031..31a9d5eb 100644 --- a/src/components/widgets/navbar/Notes.jsx +++ b/src/components/widgets/navbar/Notes.jsx @@ -25,6 +25,7 @@ export default class Notes extends React.PureComponent { }; pin() { + window.analytics.postEvent('featureUpdate', 'Feature notes pin used'); document.getElementById('noteContainer').classList.toggle('visibilityshow'); if (localStorage.getItem('notesPinned') === 'true') { @@ -35,6 +36,7 @@ export default class Notes extends React.PureComponent { } copy() { + window.analytics.postEvent('featureUpdate', 'Feature notes copy used'); // this.state.notes doesnt work for some reason navigator.clipboard.writeText(localStorage.getItem('notes')); toast(window.language.toasts.notes); diff --git a/src/components/widgets/quicklinks/QuickLinks.jsx b/src/components/widgets/quicklinks/QuickLinks.jsx index 84c39c93..6f60ed27 100644 --- a/src/components/widgets/quicklinks/QuickLinks.jsx +++ b/src/components/widgets/quicklinks/QuickLinks.jsx @@ -31,6 +31,8 @@ export default class QuickLinks extends React.PureComponent { this.setState({ items: data }); + + window.analytics.postEvent('featureUpdate', 'Feature delete quicklink used'); } addLink = () => { @@ -73,6 +75,8 @@ export default class QuickLinks extends React.PureComponent { url: '' }); + window.analytics.postEvent('featureUpdate', 'Feature add quicklink used'); + this.toggleAdd(); } diff --git a/src/components/widgets/quote/Quote.jsx b/src/components/widgets/quote/Quote.jsx index 66e7220c..a39362e4 100644 --- a/src/components/widgets/quote/Quote.jsx +++ b/src/components/widgets/quote/Quote.jsx @@ -150,11 +150,13 @@ export default class Quote extends React.PureComponent { } copyQuote = () => { + window.analytics.postEvent('featureUpdate', 'Feature quote copy used'); navigator.clipboard.writeText(`${this.state.quote} - ${this.state.author}`); toast(window.language.toasts.quote); } tweetQuote = () => { + window.analytics.postEvent('featureUpdate', 'Feature quote tweet used'); window.open(`https://twitter.com/intent/tweet?text=${this.state.quote} - ${this.state.author} on @getmue`, '_blank').focus(); } @@ -170,6 +172,8 @@ export default class Quote extends React.PureComponent { favourited: }); } + + window.analytics.postEvent('featureUpdate', 'Feature quote favourite used'); } init() { diff --git a/src/components/widgets/search/Search.jsx b/src/components/widgets/search/Search.jsx index e2e47113..0f67000e 100644 --- a/src/components/widgets/search/Search.jsx +++ b/src/components/widgets/search/Search.jsx @@ -44,6 +44,7 @@ export default class Search extends React.PureComponent { } setTimeout(() => { + window.analytics.postEvent('featureUpdate', 'Feature voice search used'); window.location.href = this.state.url + `?${this.state.query}=` + searchText.value; }, 1000); }; @@ -58,6 +59,7 @@ export default class Search extends React.PureComponent { value = document.getElementById('searchtext').value || 'mue fast'; } + window.analytics.postEvent('featureUpdate', 'Feature search used'); window.location.href = this.state.url + `?${this.state.query}=` + value; } diff --git a/src/index.js b/src/index.js index cf976a73..bd9c64e9 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,9 @@ import 'react-toastify/dist/ReactToastify.min.css'; import '@fontsource/lexend-deca/400.css'; +// this is opt-in btw +import Analytics from './modules/helpers/analytics'; + // language import merge from '@material-ui/utils/esm/deepmerge'; @@ -36,6 +39,14 @@ if (window.languagecode !== 'en_GB' || window.languagecode !== 'en_US') { } window.constants = Constants; +if (localStorage.getItem('analytics') === 'true' && localStorage.getItem('offlineMode') !== 'true') { + window.analytics = new Analytics(window.constants.UMAMI_ID); +} else { + window.analytics = { + tabLoad: () => '', + postEvent: () => '' + } +} ReactDOM.render( , diff --git a/src/modules/constants.js b/src/modules/constants.js index ade3c3f8..ac9e93d1 100644 --- a/src/modules/constants.js +++ b/src/modules/constants.js @@ -9,6 +9,8 @@ export const GITHUB_URL = 'https://api.github.com'; export const BLOG_POST = 'https://blog.muetab.com/posts/version-5-1'; export const FEEDBACK_FORM = 'https://api.formcake.com/api/form/349b56cb-7e2b-4004-b32b-e8964d217dd1/submission'; export const DDG_PROXY = 'https://external-content.duckduckgo.com/iu/?u='; +export const UMAMI_DOMAIN = 'https://umami.muetab.com'; +export const UMAMI_ID = '1b97e723-199c-48d8-8992-17c4e22d4f3c'; export const OFFLINE_IMAGES = 20; export const BETA_VERSION = false; export const VERSION = '5.1.0'; diff --git a/src/modules/helpers/analytics.js b/src/modules/helpers/analytics.js new file mode 100644 index 00000000..f3ea767f --- /dev/null +++ b/src/modules/helpers/analytics.js @@ -0,0 +1,49 @@ +export default class Analytics { + constructor(id) { + this.id = id; + this.domain = window.constants.UMAMI_DOMAIN; + } + + async postEvent(type, name) { + await fetch(this.domain + '/api/collect', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + type: 'event', + payload: { + website: this.id, + url: '/', + event_type: type, + event_value: name.toLowerCase().replaceAll(' ', '-'), + hostname: 'localhost', + language: localStorage.getItem('language').replace('_', '-'), + screen: `${window.screen.width}x${window.screen.height}` + } + }) + }); + } + + async tabLoad() { + await fetch(this.domain + '/api/collect', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + type: 'pageview', + payload: { + website: this.id, + url: '/', + referrer: '', + hostname: 'localhost', + language: localStorage.getItem('language').replace('_', '-'), + screen: `${window.screen.width}x${window.screen.height}` + } + }) + }); + } +} \ No newline at end of file