diff --git a/.env.dev b/.env.dev index bc2d667b1..e27084a69 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 97362e827..c3aac8eac 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 d64eeb5fd..c7ebc4ed3 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 6f821dfba..9e17267bb 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 000000000..e69de29bb 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 000000000..1d93c383e --- /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 000000000..2f2b46b87 --- /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 000000000..44ee22873 --- /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 ce0eb9b85..f38da9629 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 6c0cffa4e..4d7a6fa98 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 000000000..c89bfa349 --- /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 9cbe278e8..5bdfeb9c6 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 c39805d4f..ac3895f6d 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 afaea8215..98ad48d3f 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -22,15 +22,26 @@ + + diff --git a/front/src/components/Raven.vue b/front/src/components/Raven.vue new file mode 100644 index 000000000..e5e125b81 --- /dev/null +++ b/front/src/components/Raven.vue @@ -0,0 +1,41 @@ + + + + + + diff --git a/front/src/store/index.js b/front/src/store/index.js index a5df7c240..74f9d42b1 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 000000000..a0071f096 --- /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 000000000..4b06cb5f0 --- /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) + }) + }) +})