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)
+ })
+ })
+})