diff --git a/docs/examples/webextension/background.js b/docs/examples/webextension/background.js new file mode 100644 index 0000000000..e2dac4b000 --- /dev/null +++ b/docs/examples/webextension/background.js @@ -0,0 +1,14 @@ +fetch('https://testpilot.firefox.com/api/metrics/ping/testpilottest', { + method: 'POST', + mode: 'cors', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + "some": "random", + "metrics": "ping", + "payload": "here" + }) +}).then(resp => { + console.log('metrics ping success', resp); +}).catch(e => { + console.log('problem sending metrics ping', e); +}); diff --git a/docs/examples/webextension/icons/icon-32.png b/docs/examples/webextension/icons/icon-32.png new file mode 100644 index 0000000000..48cc342219 Binary files /dev/null and b/docs/examples/webextension/icons/icon-32.png differ diff --git a/docs/examples/webextension/manifest.json b/docs/examples/webextension/manifest.json new file mode 100644 index 0000000000..f435491613 --- /dev/null +++ b/docs/examples/webextension/manifest.json @@ -0,0 +1,19 @@ +{ + "manifest_version": 2, + "name": "Test Pilot WebExtension Example", + "version": "1.0", + "description": "This is a WebExtension built as an example Test Pilot experiment", + "icons": { + "32": "icons/icon-32.png" + }, + "permissions": ["background"], + "applications": { + "gecko": { + "id": "testpilotexample1@mozilla.org", + "strict_min_version": "45.0" + } + }, + "background": { + "scripts": ["background.js"] + } +} diff --git a/requirements.txt b/requirements.txt index 61cc43f996..66a890295e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -189,3 +189,5 @@ decorator==3.3.2 \ --hash=sha256:c878e3c9a4015893fddcc7a145017bd54bd51cda0eb234cab6a20fa02540cb1f django-mozilla-product-details==0.11.1 \ --hash=sha256:6a9bf2898deae722876bd19eb874bfdcbe884f99c8096c2ddd6cdea8b95d4fa0 +django-cors-headers==1.1.0 \ + --hash=sha256:fcd96e2be47c8eef34c650e007a6d546e19e7ee61041b89edbbbbe7619aa3987 diff --git a/testpilot/metrics/__init__.py b/testpilot/metrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testpilot/metrics/tests.py b/testpilot/metrics/tests.py new file mode 100644 index 0000000000..7b6329c823 --- /dev/null +++ b/testpilot/metrics/tests.py @@ -0,0 +1,90 @@ +from django.test.utils import override_settings +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User + +import json + +from mozilla_cloud_services_logger.formatters import JsonLogFormatter + +from testfixtures import LogCapture +from ..utils import TestCase + + +class LogPingTestCase(TestCase): + + def setUp(self): + super(LogPingTestCase, self).setUp() + + self.handler = LogCapture() + + self.username = 'johndoe2' + self.password = 'trustno1' + self.email = '%s@example.com' % self.username + + self.user = User.objects.create_user( + username=self.username, + email=self.email, + password=self.password) + + def tearDown(self): + self.handler.uninstall() + + def test_404_ping(self): + """POST to /api/metrics/foobar should be a 404""" + self.client.login(username=self.username, + password=self.password) + log_name = 'foobar' + url = reverse('log-ping', args=(log_name,)) + resp = self.client.post(url, {}) + self.assertEqual(404, resp.status_code) + + def test_unauth_ping(self): + log_name = 'testpilottest' + data = { + "service": "wayforwardthing", + "agent": "frobnitz browser" + } + url = reverse('log-ping', args=(log_name,)) + + self.handler.records = [] + resp = self.client.post( + url, + content_type='application/json', + data=json.dumps(data)) + + self.assertEqual(200, resp.status_code) + + record = self.handler.records[0] + formatter = JsonLogFormatter(logger_name=record.name) + details = json.loads(formatter.format(record)) + fields = details['Fields'] + + self.assertEqual(log_name, record.name) + self.assertEqual(fields['service'], data['service']) + self.assertEqual(fields['agent'], data['agent']) + + @override_settings(LOG_PING_WHITELIST=['alpha', 'beta', 'delta']) + def test_auth_ping(self): + """POST to /api/metrics/{name} should result in expected log event""" + self.client.login(username=self.username, + password=self.password) + + for log_name in ['alpha', 'beta', 'delta']: + url = reverse('log-ping', args=(log_name,)) + + self.handler.records = [] + resp = self.client.post( + url, + content_type='application/json', + data=json.dumps({'context': 'wheee'})) + + self.assertEqual(200, resp.status_code) + + record = self.handler.records[0] + formatter = JsonLogFormatter(logger_name=record.name) + details = json.loads(formatter.format(record)) + fields = details['Fields'] + + self.assertEqual(log_name, record.name) + self.assertEqual(fields['uid'], self.user.id) + self.assertEqual(fields['context'], 'wheee') diff --git a/testpilot/metrics/urls.py b/testpilot/metrics/urls.py new file mode 100644 index 0000000000..aefdd05b49 --- /dev/null +++ b/testpilot/metrics/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url, patterns + +from . import views + +urlpatterns = patterns( + '', + url(r'^ping/(?P.+)', views.log_ping, name='log-ping'), +) diff --git a/testpilot/metrics/views.py b/testpilot/metrics/views.py new file mode 100644 index 0000000000..da2e923b6b --- /dev/null +++ b/testpilot/metrics/views.py @@ -0,0 +1,30 @@ +import logging + +from django.conf import settings +from django.http import Http404 + +from django.views.decorators.csrf import csrf_exempt +from rest_framework.response import Response +from rest_framework.decorators import permission_classes, api_view + +DEFAULT_LOG_PING_WHITELIST = [ + 'testpilottest' +] + + +@csrf_exempt +@api_view(['POST']) +@permission_classes([]) +def log_ping(request, logger_name): + """Accept and log metrics pings""" + whitelist = getattr(settings, 'LOG_PING_WHITELIST', DEFAULT_LOG_PING_WHITELIST) + if logger_name not in whitelist: + raise Http404 + + extra = {} + if request.user.is_authenticated(): + extra['uid'] = request.user.id + extra.update(request.data) + logging.getLogger(logger_name).info('', extra=extra) + + return Response({'status': 'ok'}) diff --git a/testpilot/settings.py b/testpilot/settings.py index 973106f532..2383ab592e 100644 --- a/testpilot/settings.py +++ b/testpilot/settings.py @@ -69,6 +69,7 @@ def path(*args): 'testpilot.base', 'testpilot.frontend', 'testpilot.users', + 'testpilot.metrics', 'testpilot.experiments', # Third party apps @@ -76,6 +77,7 @@ def path(*args): 'django_cleanup', 'csp', + 'corsheaders', 'colorfield', 'rest_framework', 'storages', @@ -108,6 +110,7 @@ def path(*args): MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -423,6 +426,9 @@ def lazy_langs(): 'https://*.mozilla.net', ) +CORS_ORIGIN_ALLOW_ALL = True +CORS_URLS_REGEX = r'^/api/.*$' + LOGGING = { 'version': 1, 'disable_existing_loggers': False, @@ -464,6 +470,11 @@ def lazy_langs(): 'level': config('DJANGO_LOG_LEVEL', default='INFO'), 'propagate': True, }, + 'testpilottest': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': True, + }, 'request.summary': { 'handlers': ['console'], 'level': 'INFO', diff --git a/testpilot/urls.py b/testpilot/urls.py index a2b82209c9..b03ef8ec62 100644 --- a/testpilot/urls.py +++ b/testpilot/urls.py @@ -19,6 +19,7 @@ url(r'^admin/', include(admin.site.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + url(r'^api/metrics/', include('testpilot.metrics.urls')), url(r'^api/experiments/', include('testpilot.experiments.urls')), url(r'^api/', include(router.urls)), # Catch-all fallback to frontend client view