diff --git a/.env.dev b/.env.dev index bc2d667b1d4bbb61b6e81583c981debb229d3204..e27084a69d420605153126965060d66171b3a7f3 100644 --- a/.env.dev +++ b/.env.dev @@ -1,3 +1,5 @@ BACKEND_URL=http://localhost:6001 API_AUTHENTICATION_REQUIRED=True CACHALOT_ENABLED=False +RAVEN_ENABLED=false +RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5 diff --git a/CHANGELOG b/CHANGELOG index 97362e827c7759ff3add3776be427d7e24929fe7..c3aac8eacdf0c925e6978830b1e375df4d9c8f8a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,11 @@ Changelog - Front: added some unittests for the store (#55) - Front: fixed broken login redirection when 401 - Front: Removed autoplay on page reload +- API: Added a /instance/settings endpoint +- Front: load /instance/settings on page load +- Added settings to report JS and Python error to a Sentry instance + This is disabled by default, but feel free to enable it if you want + to help us by sending your error reports :) (#8) 0.3.5 (2018-01-07) diff --git a/api/config/api_urls.py b/api/config/api_urls.py index d64eeb5fdbb62b5cdfaceaef8740a82cc9d9c2ef..c7ebc4ed3668196ccd11d6844ba14d704e3ba43e 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -1,5 +1,6 @@ from rest_framework import routers from django.conf.urls import include, url +from funkwhale_api.instance import views as instance_views from funkwhale_api.music import views from funkwhale_api.playlists import views as playlists_views from rest_framework_jwt import views as jwt_views @@ -25,6 +26,10 @@ router.register( v1_patterns = router.urls v1_patterns += [ + url(r'^instance/', + include( + ('funkwhale_api.instance.urls', 'instance'), + namespace='instance')), url(r'^providers/', include( ('funkwhale_api.providers.urls', 'providers'), diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 6f821dfba4de6a3b8af13dd7273c7a3e213cbf2c..9e17267bb98cfd06ec4cadaa08370641126275c8 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -12,6 +12,7 @@ from __future__ import absolute_import, unicode_literals import os import environ +from funkwhale_api import __version__ ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /) APPS_DIR = ROOT_DIR.path('funkwhale_api') @@ -56,10 +57,28 @@ THIRD_PARTY_APPS = ( 'django_filters', ) + +# Sentry +RAVEN_ENABLED = env.bool("RAVEN_ENABLED", default=False) +RAVEN_DSN = env("RAVEN_DSN", default='') + +if RAVEN_ENABLED: + RAVEN_CONFIG = { + 'dsn': RAVEN_DSN, + # If you are using git, you can also automatically configure the + # release based on the git info. + 'release': __version__, + } + THIRD_PARTY_APPS += ( + 'raven.contrib.django.raven_compat', + ) + + # Apps specific for this project go here. LOCAL_APPS = ( 'funkwhale_api.users', # custom users app # Your stuff: custom apps go here + 'funkwhale_api.instance', 'funkwhale_api.music', 'funkwhale_api.favorites', 'funkwhale_api.radios', @@ -71,6 +90,7 @@ LOCAL_APPS = ( ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps + INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS # MIDDLEWARE CONFIGURATION diff --git a/api/funkwhale_api/instance/__init__.py b/api/funkwhale_api/instance/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/funkwhale_api/instance/dynamic_preferences_registry.py b/api/funkwhale_api/instance/dynamic_preferences_registry.py new file mode 100644 index 0000000000000000000000000000000000000000..1d93c383eb80372c507862a3b4f2e3450d792fca --- /dev/null +++ b/api/funkwhale_api/instance/dynamic_preferences_registry.py @@ -0,0 +1,37 @@ +from dynamic_preferences import types +from dynamic_preferences.registries import global_preferences_registry + +raven = types.Section('raven') + + +@global_preferences_registry.register +class RavenDSN(types.StringPreference): + show_in_api = True + section = raven + name = 'front_dsn' + default = 'https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4' + verbose_name = ( + 'A raven DSN key used to report front-ent errors to ' + 'a sentry instance' + ) + help_text = ( + 'Keeping the default one will report errors to funkwhale developers' + ) + + +SENTRY_HELP_TEXT = ( + 'Error reporting is disabled by default but you can enable it if' + ' you want to help us improve funkwhale' +) + + +@global_preferences_registry.register +class RavenEnabled(types.BooleanPreference): + show_in_api = True + section = raven + name = 'front_enabled' + default = False + verbose_name = ( + 'Wether error reporting to a Sentry instance using raven is enabled' + ' for front-end errors' + ) diff --git a/api/funkwhale_api/instance/urls.py b/api/funkwhale_api/instance/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..2f2b46b87a4fe301387c5e134cc7a6fcdf6291b2 --- /dev/null +++ b/api/funkwhale_api/instance/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from . import views + + +urlpatterns = [ + url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'), +] diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py new file mode 100644 index 0000000000000000000000000000000000000000..44ee228735d0ad7297dcf31d513ecc087d8c9d59 --- /dev/null +++ b/api/funkwhale_api/instance/views.py @@ -0,0 +1,25 @@ +from rest_framework import views +from rest_framework.response import Response + +from dynamic_preferences.api import serializers +from dynamic_preferences.registries import global_preferences_registry + + +class InstanceSettings(views.APIView): + permission_classes = [] + authentication_classes = [] + + def get(self, request, *args, **kwargs): + manager = global_preferences_registry.manager() + manager.all() + all_preferences = manager.model.objects.all().order_by( + 'section', 'name' + ) + api_preferences = [ + p + for p in all_preferences + if getattr(p.preference, 'show_in_api', False) + ] + data = serializers.GlobalPreferenceSerializer( + api_preferences, many=True).data + return Response(data, status=200) diff --git a/api/requirements/base.txt b/api/requirements/base.txt index ce0eb9b85f14bfcef9d92b3e1084ca084375c2a9..f38da9629041fcd8f9f1a0620d00ec17c0823f32 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -56,3 +56,4 @@ git+https://github.com/EliotBerriot/django-cachalot.git@django-2 django-dynamic-preferences>=1.5,<1.6 pyacoustid>=1.1.5,<1.2 +raven>=6.5,<7 diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 6c0cffa4e0da24ed87501a692464598563de2480..4d7a6fa981d1431cd01a97d4578d2a992b4cb749 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -3,6 +3,7 @@ import shutil import pytest from django.core.cache import cache as django_cache from dynamic_preferences.registries import global_preferences_registry +from rest_framework.test import APIClient from funkwhale_api.taskapp import celery @@ -29,7 +30,9 @@ def factories(db): @pytest.fixture def preferences(db): - yield global_preferences_registry.manager() + manager = global_preferences_registry.manager() + manager.all() + yield manager @pytest.fixture @@ -48,6 +51,11 @@ def logged_in_client(db, factories, client): delattr(client, 'user') +@pytest.fixture +def api_client(client): + return APIClient() + + @pytest.fixture def superuser_client(db, factories, client): user = factories['users.SuperUser']() diff --git a/api/tests/instance/test_preferences.py b/api/tests/instance/test_preferences.py new file mode 100644 index 0000000000000000000000000000000000000000..c89bfa3492091a93d0000f661bd83f2fc8ebedc0 --- /dev/null +++ b/api/tests/instance/test_preferences.py @@ -0,0 +1,22 @@ +from django.urls import reverse + +from dynamic_preferences.api import serializers + + +def test_can_list_settings_via_api(preferences, api_client): + url = reverse('api:v1:instance:settings') + all_preferences = preferences.model.objects.all() + expected_preferences = { + p.preference.identifier(): p + for p in all_preferences + if getattr(p.preference, 'show_in_api', False)} + + assert len(expected_preferences) > 0 + + response = api_client.get(url) + assert response.status_code == 200 + assert len(response.data) == len(expected_preferences) + + for p in response.data: + i = '__'.join([p['section'], p['name']]) + assert i in expected_preferences diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index 9cbe278e827e96b015945b7a3beac1c6d1606d63..5bdfeb9c626fa074eaedb5c7f499871946d3dea9 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -78,3 +78,10 @@ API_AUTHENTICATION_REQUIRED=True # public: anybody can register an account # disabled: nobody can register an account REGISTRATION_MODE=disabled + +# Sentry/Raven error reporting (server side) +# Enable Raven if you want to help improve funkwhale by +# automatically sending error reports our Sentry instance. +# This will help us detect and correct bugs +RAVEN_ENABLED=false +RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5 diff --git a/front/package.json b/front/package.json index c39805d4f5a78331322c0d460ba62950f637957b..ac3895f6d65655fd198699c92cd55300df301b1e 100644 --- a/front/package.json +++ b/front/package.json @@ -21,6 +21,7 @@ "jwt-decode": "^2.2.0", "lodash": "^4.17.4", "moxios": "^0.4.0", + "raven-js": "^3.22.3", "semantic-ui-css": "^2.2.10", "vue": "^2.3.3", "vue-lazyload": "^1.1.4", diff --git a/front/src/App.vue b/front/src/App.vue index afaea82153987c433dfb8717922aa1aead73f003..98ad48d3ff41cd37cfa4df496c51c24f8dbd0544 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -22,15 +22,26 @@ </div> </div> </div> + <raven + v-if="$store.state.instance.settings.raven.front_enabled.value" + :dsn="$store.state.instance.settings.raven.front_dsn.value"> + </raven> </div> </template> <script> import Sidebar from '@/components/Sidebar' +import Raven from '@/components/Raven' export default { name: 'app', - components: { Sidebar } + components: { + Sidebar, + Raven + }, + created () { + this.$store.dispatch('instance/fetchSettings') + } } </script> diff --git a/front/src/components/Raven.vue b/front/src/components/Raven.vue new file mode 100644 index 0000000000000000000000000000000000000000..e5e125b810ec0cb53e4080d64f3b0c9133305307 --- /dev/null +++ b/front/src/components/Raven.vue @@ -0,0 +1,41 @@ +<template> + <div class="raven"></div> +</template> + +<script> +import Raven from 'raven-js' +import RavenVue from 'raven-js/plugins/vue' +import Vue from 'vue' +import logger from '@/logging' + +export default { + props: ['dsn'], + created () { + Raven.uninstall() + this.setUp() + }, + destroyed () { + Raven.uninstall() + }, + methods: { + setUp () { + Raven.uninstall() + logger.default.info('Installing raven...') + Raven.config(this.dsn).addPlugin(RavenVue, Vue).install() + console.log({}.test.test) + } + }, + watch: { + dsn: function () { + this.setUp() + } + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped > +.raven { + display: none; +} +</style> diff --git a/front/src/store/index.js b/front/src/store/index.js index a5df7c2406e08daf0712aa940856233085ec4f61..74f9d42b195be8d38aaa8f6a5745c895f029a346 100644 --- a/front/src/store/index.js +++ b/front/src/store/index.js @@ -4,6 +4,7 @@ import createPersistedState from 'vuex-persistedstate' import favorites from './favorites' import auth from './auth' +import instance from './instance' import queue from './queue' import radios from './radios' import player from './player' @@ -14,6 +15,7 @@ export default new Vuex.Store({ modules: { auth, favorites, + instance, queue, radios, player diff --git a/front/src/store/instance.js b/front/src/store/instance.js new file mode 100644 index 0000000000000000000000000000000000000000..a0071f0961d6536f2133331787d90245a6dc1df4 --- /dev/null +++ b/front/src/store/instance.js @@ -0,0 +1,42 @@ +import axios from 'axios' +import logger from '@/logging' +import _ from 'lodash' + +export default { + namespaced: true, + state: { + settings: { + raven: { + front_enabled: { + value: false + }, + front_dsn: { + value: null + } + } + } + }, + mutations: { + settings: (state, value) => { + _.merge(state.settings, value) + } + }, + actions: { + // Send a request to the login URL and save the returned JWT + fetchSettings ({commit}) { + return axios.get('instance/settings/').then(response => { + logger.default.info('Successfully fetched instance settings') + let sections = {} + response.data.forEach(e => { + sections[e.section] = {} + }) + response.data.forEach(e => { + sections[e.section][e.name] = e + }) + commit('settings', sections) + }, response => { + logger.default.error('Error while fetching settings', response.data) + }) + } + } +} diff --git a/front/test/unit/specs/store/instance.spec.js b/front/test/unit/specs/store/instance.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..4b06cb5f029bc0b2180cb4e3aa90854eda4b73b2 --- /dev/null +++ b/front/test/unit/specs/store/instance.spec.js @@ -0,0 +1,70 @@ +var sinon = require('sinon') +import moxios from 'moxios' +import store from '@/store/instance' +import { testAction } from '../../utils' + +describe('store/instance', () => { + var sandbox + + beforeEach(function () { + sandbox = sinon.sandbox.create() + moxios.install() + }) + afterEach(function () { + sandbox.restore() + moxios.uninstall() + }) + + describe('mutations', () => { + it('settings', () => { + const state = {settings: {raven: {front_dsn: {value: 'test'}}}} + let settings = {raven: {front_enabled: {value: true}}} + store.mutations.settings(state, settings) + expect(state.settings).to.deep.equal({ + raven: {front_dsn: {value: 'test'}, front_enabled: {value: true}} + }) + }) + }) + describe('actions', () => { + it('fetchSettings', (done) => { + moxios.stubRequest('instance/settings/', { + status: 200, + response: [ + { + section: 'raven', + name: 'front_dsn', + value: 'test' + }, + { + section: 'raven', + name: 'front_enabled', + value: false + } + ] + }) + testAction({ + action: store.actions.fetchSettings, + payload: null, + expectedMutations: [ + { + type: 'settings', + payload: { + raven: { + front_dsn: { + section: 'raven', + name: 'front_dsn', + value: 'test' + }, + front_enabled: { + section: 'raven', + name: 'front_enabled', + value: false + } + } + } + } + ] + }, done) + }) + }) +})