diff --git a/README.md b/README.md
index ba7de4c..81a457d 100644
--- a/README.md
+++ b/README.md
@@ -123,3 +123,22 @@ logger = logging.getLogger('django')
logger.warning('Something happened')
```
+
+
+## **Real User Monitoring (RUM)**
+
+Real user monitoring injects a script tag containing the [RUM JS](https://stackify.com/retrace-real-user-monitoring/) that is responsible for capturing information about the http requests on the browser. This approach is manual and needs to be configured.
+
+### RUM - Setup
+
+```python
+# Configuration - Standard API
+logger = stackify.getLogger(..., rum_key="YourRumKey")
+# or Configuration - Python Logging Integration
+stackify.StackifyHandler(..., rum_key="YourRumKey")
+
+# Use this to apply on views
+import stackify.rum
+
+stackify.rum.insert_rum_script()
+```
\ No newline at end of file
diff --git a/docker/stackify-python-api-test b/docker/stackify-python-api-test
index d7d7957..7383754 100644
--- a/docker/stackify-python-api-test
+++ b/docker/stackify-python-api-test
@@ -3,6 +3,8 @@ ARG from_version
FROM python:${from_version}
ARG version
+ARG test
+ARG test_repo
RUN \
apt-get update && \
@@ -14,4 +16,7 @@ COPY . /build/
RUN cat /build/requirements.txt | xargs -n 1 pip install; exit 0
+ENV TEST="${test}"
+ENV TEST_REPO="${test_repo}"
+
CMD /bin/bash -c "cd /build && source test-docker-execute.sh"
diff --git a/stackify/compat.py b/stackify/compat.py
new file mode 100644
index 0000000..6aa68f7
--- /dev/null
+++ b/stackify/compat.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+import sys
+import types
+
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+
+if PY2:
+ import StringIO
+ import Queue as queue # noqa F401
+ import urlparse # noqa F401
+ from urllib2 import HTTPError # noqa F401
+ from urllib import unquote as unquote_core # noqa F401
+
+ StringIO = BytesIO = StringIO.StringIO
+
+ string_types = (basestring,) # noqa F821
+ integer_types = (int, long) # noqa F821
+ class_types = (type, types.ClassType)
+ text_type = unicode # noqa F821
+ binary_type = str
+ list_type = list
+ dict_type = dict
+
+ def b(s):
+ return s
+
+ def iterkeys(d, **kwargs):
+ return d.iterkeys(**kwargs)
+
+ def iteritems(d, **kwargs):
+ return d.iteritems(**kwargs)
+
+ def iterlists(d, **kwargs):
+ return d.iterlists(**kwargs)
+
+ def unquote(*args, **kwargs): # noqa F811
+ return unquote_core(*args, **kwargs)
+else:
+ import io
+ import queue # noqa F401
+ from urllib import parse as urlparse # noqa F401
+ from urllib.error import HTTPError # noqa F401
+
+ StringIO = io.StringIO
+ BytesIO = io.BytesIO
+
+ string_types = (str,)
+ integer_types = (int,)
+ class_types = (type,)
+ text_type = str
+ binary_type = bytes
+ list_type = list
+ dict_type = dict
+
+ def b(s):
+ return s.encode("latin-1")
+
+ def iterkeys(d, **kwargs):
+ return iter(d.keys(**kwargs))
+
+ def iteritems(d, **kwargs):
+ return iter(d.items(**kwargs))
+
+ def iterlists(d, **kwargs):
+ return iter(d.lists(**kwargs))
+
+ def unquote(*args, **kwargs):
+ return urlparse.unquote(*args, **kwargs)
+
+
+def multidict_to_dict(d):
+ return dict((k, v[0] if len(v) == 1 else v) for k, v in iterlists(d))
diff --git a/stackify/config.py b/stackify/config.py
new file mode 100644
index 0000000..58d5fa3
--- /dev/null
+++ b/stackify/config.py
@@ -0,0 +1,6 @@
+from stackify.constants import DEFAULT_RUM_KEY, DEFAULT_RUM_SCRIPT_URL
+
+rum_key = DEFAULT_RUM_KEY
+rum_script_url = DEFAULT_RUM_SCRIPT_URL
+application = None
+environment = None
diff --git a/stackify/constants.py b/stackify/constants.py
index 3dce49e..c560063 100644
--- a/stackify/constants.py
+++ b/stackify/constants.py
@@ -38,3 +38,6 @@
TRANSPORT_TYPE_DEFAULT = 'default'
TRANSPORT_TYPE_AGENT_SOCKET = 'agent_socket'
TRANSPORT_TYPE_AGENT_HTTP = 'agent_http'
+
+DEFAULT_RUM_SCRIPT_URL = "https://stckjs.stackify.com/stckjs.js"
+DEFAULT_RUM_KEY = ""
diff --git a/stackify/rum.py b/stackify/rum.py
new file mode 100644
index 0000000..10f2f76
--- /dev/null
+++ b/stackify/rum.py
@@ -0,0 +1,86 @@
+import json
+import base64
+from stackify import config
+
+apm_installed = False
+
+try:
+ from stackifyapm import insert_rum_script as insert_rum_script_from_apm
+ apm_installed = True
+except (ImportError):
+ pass
+
+
+def insert_rum_script():
+ apm_rum_script = insert_rum_script_apm()
+ if apm_rum_script is not None:
+ return apm_rum_script
+
+ rum_key = config.rum_key
+ rum_script_url = config.rum_script_url
+
+ if not rum_script_url or not rum_key:
+ return ''
+
+ transaction_id = get_transaction_id()
+ if not transaction_id:
+ return ''
+
+ reporting_url = get_reporting_url()
+ if not reporting_url:
+ return ''
+
+ application_name = config.application
+ if not application_name:
+ return ''
+
+ environment = config.environment
+ if not environment:
+ return ''
+
+ settings = {
+ "ID": transaction_id
+ }
+
+ if application_name:
+ application_name_b64 = base64.b64encode(application_name.encode("utf-8")).decode("utf-8")
+ if (application_name_b64):
+ settings["Name"] = application_name_b64
+
+ if environment:
+ environment_b64 = base64.b64encode(environment.encode("utf-8")).decode("utf-8")
+ if (environment_b64):
+ settings["Env"] = environment_b64
+
+ if reporting_url:
+ reporting_url_b64 = base64.b64encode(reporting_url.encode("utf-8")).decode("utf-8")
+ if (reporting_url_b64):
+ settings["Trans"] = reporting_url_b64
+
+ if not settings:
+ return ''
+
+ return ''.format(
+ json.dumps(settings),
+ rum_script_url,
+ rum_key
+ )
+
+
+def get_transaction_id():
+ return ''
+
+
+def get_reporting_url():
+ return ''
+
+
+def insert_rum_script_apm():
+ if not is_apm_installed():
+ return None
+
+ return insert_rum_script_from_apm()
+
+
+def is_apm_installed():
+ return apm_installed
diff --git a/stackify/transport/application.py b/stackify/transport/application.py
index ac11947..bcbdd66 100644
--- a/stackify/transport/application.py
+++ b/stackify/transport/application.py
@@ -1,5 +1,6 @@
import socket
import os
+import logging
from stackify.utils import arg_or_env
from stackify.constants import API_URL
@@ -9,6 +10,12 @@
from stackify.constants import TRANSPORT_TYPE_AGENT_SOCKET
from stackify.constants import TRANSPORT_TYPE_DEFAULT
from stackify.transport.default.formats import JSONObject
+from stackify.constants import DEFAULT_RUM_SCRIPT_URL
+from stackify.constants import DEFAULT_RUM_KEY
+from stackify.utils import RegexValidator, ConfigError
+from stackify import config
+
+internal_logger = logging.getLogger(__name__)
class EnvironmentDetail(JSONObject):
@@ -38,6 +45,8 @@ def __init__(
socket_url=SOCKET_URL,
transport=None,
http_endpoint=DEFAULT_HTTP_ENDPOINT,
+ rum_script_url=DEFAULT_RUM_SCRIPT_URL,
+ rum_key=DEFAULT_RUM_KEY
):
self.api_key = api_key
self.api_url = api_url
@@ -47,6 +56,39 @@ def __init__(
self.http_endpoint = http_endpoint
self.transport = transport
+ self.rum_script_url = DEFAULT_RUM_SCRIPT_URL
+ self.rum_key = DEFAULT_RUM_KEY
+
+ # Rum config validation
+ if rum_script_url != DEFAULT_RUM_SCRIPT_URL:
+ self.validate(
+ RegexValidator("^((((https?|ftps?|gopher|telnet|nntp)://)|(mailto:|news:))(%[0-9A-Fa-f]{2}|[-\(\)_.!~*';/?:@&=+$,A-Za-z0-9])+)([).!';/?:,][\[:blank:|:blank:\]])?$"),
+ rum_script_url,
+ 'rum_script_url'
+ )
+ config.rum_script_url = self.rum_script_url
+
+ if rum_key != DEFAULT_RUM_KEY:
+ self.validate(
+ RegexValidator("^[A-Za-z0-9_-]+$"),
+ rum_key,
+ 'rum_key'
+ )
+ config.rum_key = self.rum_key
+
+ config.environment = self.environment
+ config.application = self.application
+
+ def validate(self, validator, value, key):
+ if not validator:
+ return
+
+ try:
+ value = validator(value, key)
+ setattr(self, key, str(value))
+ except ConfigError as e:
+ internal_logger.exception(str(e))
+
def get_configuration(**kwargs):
"""
@@ -69,4 +111,6 @@ def get_configuration(**kwargs):
socket_url=arg_or_env('socket_url', kwargs, SOCKET_URL),
http_endpoint=arg_or_env('http_endpoint', kwargs, DEFAULT_HTTP_ENDPOINT, env_key='STACKIFY_TRANSPORT_HTTP_ENDPOINT'),
transport=transport,
+ rum_script_url=arg_or_env('rum_script_url', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_SCRIPT_URL'),
+ rum_key=arg_or_env('rum_key', kwargs, DEFAULT_RUM_KEY, env_key='RETRACE_RUM_KEY')
)
diff --git a/stackify/utils.py b/stackify/utils.py
index 2da8944..0a092a3 100644
--- a/stackify/utils.py
+++ b/stackify/utils.py
@@ -1,6 +1,8 @@
import os
import json
import logging
+import re
+from stackify import compat
internal_logger = logging.getLogger(__name__)
@@ -36,3 +38,22 @@ def get_default_object(obj):
def object_is_iterable(obj):
return hasattr(obj, '__iter__') or isinstance(obj, str)
+
+
+class RegexValidator(object):
+ def __init__(self, regex, verbose_pattern=None):
+ self.regex = regex
+ self.verbose_pattern = verbose_pattern or regex
+
+ def __call__(self, value, field_name):
+ value = compat.text_type(value)
+ match = re.match(self.regex, value)
+ if match:
+ return value
+ raise ConfigError("{} does not match pattern {}".format(value, self.verbose_pattern), field_name)
+
+
+class ConfigError(ValueError):
+ def __init__(self, msg, field_name):
+ self.field_name = field_name
+ super(ValueError, self).__init__(msg)
diff --git a/test-docker-execute.sh b/test-docker-execute.sh
index fc76dce..f87161f 100755
--- a/test-docker-execute.sh
+++ b/test-docker-execute.sh
@@ -14,7 +14,16 @@ function runPyTest() {
echo '<--------------------------------------------->'
echo "Python Version $(python --version)"
echo 'Running pytest...'
- py.test
+ py.test --ignore=tests/rum
+
+ if [ "${TEST}" = 1 ]; then
+ pip install -i "${TEST_REPO}" stackify-python-apm;
+ else
+ pip install stackify-python-apm;
+ py.test tests/rum
+ fi
+
+ pip uninstall -y stackify-python-apm
}
runFlake8
diff --git a/test-docker.sh b/test-docker.sh
index 5e0bbce..e4f4dea 100755
--- a/test-docker.sh
+++ b/test-docker.sh
@@ -18,7 +18,7 @@ do
fi
echo "Building stackify-python-api-test-${i}..."
- docker build --no-cache --build-arg from_version=${i} --build-arg version=${i} --file docker/stackify-python-api-test . -t stackify-python-api-test-${i}:latest
+ docker build --no-cache --build-arg from_version=${i} --build-arg version=${i} --build-arg test=${TEST} --build-arg test_repo=${TEST_REPO} --file docker/stackify-python-api-test . -t stackify-python-api-test-${i}:latest
echo "Running stackify-python-api-test-${i}..."
docker run --network="host" --name "stackify-python-api-test-${i}" stackify-python-api-test-${i}:latest
diff --git a/tests/bases.py b/tests/bases.py
index 0b11585..d7298bd 100644
--- a/tests/bases.py
+++ b/tests/bases.py
@@ -23,6 +23,8 @@ def setUp(self):
'STACKIFY_API_URL',
'STACKIFY_TRANSPORT',
'STACKIFY_TRANSPORT_HTTP_ENDPOINT',
+ 'RETRACE_RUM_SCRIPT_URL',
+ 'RETRACE_RUM_KEY'
]
self.saved = {}
for key in to_save:
diff --git a/tests/rum/__init__.py b/tests/rum/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/rum/test_rum_apm.py b/tests/rum/test_rum_apm.py
new file mode 100644
index 0000000..0a9a699
--- /dev/null
+++ b/tests/rum/test_rum_apm.py
@@ -0,0 +1,77 @@
+try:
+ from unittest import mock
+except Exception:
+ import mock # noqa F401
+
+import stackify
+import stackify.rum
+import base64
+import json
+from unittest import TestCase
+
+apmExist = False
+try:
+ from stackifyapm.base import Client
+ apmExist = True
+except (ImportError):
+ pass
+
+APM_CONFIG = {
+ "SERVICE_NAME": "service_name",
+ "ENVIRONMENT": "production",
+ "HOSTNAME": "sample_host",
+ "FRAMEWORK_NAME": "framework",
+ "FRAMEWORK_VERSION": "1.0",
+ "APPLICATION_NAME": "sample_application",
+ "BASE_DIR": "path/to/application/",
+ "RUM_SCRIPT_URL": "https://test.com/test.js",
+ "RUM_KEY": "LOREM123"
+}
+
+
+class RumTestApm(TestCase):
+ def setUp(self):
+ self.config_rum_key = stackify.config.rum_key
+ self.config_rum_script_url = stackify.config.rum_script_url
+ self.config_application = stackify.config.application
+ self.config_environment = stackify.config.environment
+ self.maxDiff = None
+
+ def shutDown(self):
+ pass
+
+ def test_default_insert_rum_script_from_apm_with_transaction(self):
+ if not apmExist:
+ return
+
+ client = Client(APM_CONFIG)
+ print('config')
+ print(client.config.rum_key)
+
+ transaction = client.begin_transaction("transaction_test", client=client)
+ rum_data = stackify.rum.insert_rum_script()
+
+ rum_settings = {
+ "ID": transaction.get_trace_parent().trace_id,
+ "Name": base64.b64encode(APM_CONFIG["APPLICATION_NAME"].encode("utf-8")).decode("utf-8"),
+ "Env": base64.b64encode(APM_CONFIG["ENVIRONMENT"].encode("utf-8")).decode("utf-8"),
+ "Trans": base64.b64encode('/'.encode("utf-8")).decode("utf-8")
+ }
+
+ result_string = ''.format(
+ json.dumps(rum_settings),
+ APM_CONFIG["RUM_SCRIPT_URL"],
+ APM_CONFIG["RUM_KEY"]
+ )
+
+ assert rum_data
+ assert rum_data == result_string
+ client.end_transaction("transaction_test")
+
+ def test_default_insert_rum_script_from_apm_without_transaction(self):
+ if not apmExist:
+ return
+
+ rum_data = stackify.rum.insert_rum_script()
+ assert not rum_data
+ assert rum_data is ''
diff --git a/tests/test_compat.py b/tests/test_compat.py
new file mode 100644
index 0000000..d2dcfbd
--- /dev/null
+++ b/tests/test_compat.py
@@ -0,0 +1,38 @@
+from unittest import TestCase
+
+from stackify.compat import b
+from stackify.compat import iterkeys
+from stackify.compat import iteritems
+
+
+class CompatTest(TestCase):
+ def setUp(self):
+ self.dict_data = {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ self.list_data = ["foo", "bar"]
+
+ def test_convert_string_to_byte(self):
+ byte = '1'
+
+ value = b(byte)
+
+ assert isinstance(value, bytes)
+
+ def test_iterkeys_should_return_iterator(self):
+ iter_keys = iterkeys(self.dict_data)
+
+ self.assert_instance_is_an_iterator(iter_keys)
+
+ def test_iteritems_should_return_iterator(self):
+ iter_items = iteritems(self.dict_data)
+
+ self.assert_instance_is_an_iterator(iter_items)
+
+ def assert_instance_is_an_iterator(self, item):
+ try:
+ iter(item)
+ assert True
+ except Exception:
+ assert False
diff --git a/tests/test_rum.py b/tests/test_rum.py
new file mode 100644
index 0000000..b1e77fb
--- /dev/null
+++ b/tests/test_rum.py
@@ -0,0 +1,252 @@
+try:
+ from unittest import mock
+except Exception:
+ import mock # noqa F401
+
+import stackify
+import os
+import stackify.rum
+import json
+
+from .bases import ClearEnvTest
+from stackify.transport.application import ApiConfiguration
+from stackify.utils import arg_or_env
+from stackify.constants import DEFAULT_RUM_SCRIPT_URL
+from unittest import TestCase
+
+APM_CONFIG = {
+ "SERVICE_NAME": "service_name",
+ "ENVIRONMENT": "production",
+ "HOSTNAME": "sample_host",
+ "FRAMEWORK_NAME": "framework",
+ "FRAMEWORK_VERSION": "1.0",
+ "APPLICATION_NAME": "sample_application",
+ "BASE_DIR": "path/to/application/",
+ "RUM_SCRIPT_URL": "https://test.com/test.js",
+ "RUM_KEY": "LOREM123"
+}
+
+
+class RumTest(TestCase):
+ def setUp(self):
+ self.config_rum_key = stackify.config.rum_key
+ self.config_rum_script_url = stackify.config.rum_script_url
+ self.config_application = stackify.config.application
+ self.config_environment = stackify.config.environment
+ self.maxDiff = None
+
+ def shutDown(self):
+ pass
+
+ @mock.patch('stackify.rum.is_apm_installed')
+ @mock.patch('stackify.rum.get_reporting_url')
+ @mock.patch('stackify.rum.get_transaction_id')
+ def test_default_insert_rum_script(self, func, func_reporting_url, func_apm):
+ func.return_value = '123'
+ func_reporting_url.return_value = 'test reporting url'
+ func_apm.return_value = False
+ self.update_common_config(
+ rum_key='asd',
+ application='app',
+ environment='env'
+ )
+
+ rum_settings = {
+ "ID": '123',
+ "Name": 'YXBw',
+ "Env": 'ZW52',
+ "Trans": 'dGVzdCByZXBvcnRpbmcgdXJs'
+ }
+
+ result = stackify.rum.insert_rum_script()
+ self.reset_common_config()
+
+ assert result == ''.format(json.dumps(rum_settings))
+
+ @mock.patch('stackify.rum.is_apm_installed')
+ def test_default_insert_rum_script_no_transaction_id(self, func_apm):
+ func_apm.return_value = False
+ self.update_common_config(
+ rum_key='asd',
+ application='app',
+ environment='env'
+ )
+
+ result = stackify.rum.insert_rum_script()
+ self.reset_common_config()
+
+ assert not result
+ assert result is ''
+
+ @mock.patch('stackify.rum.is_apm_installed')
+ def test_default_insert_rum_script_no_key(self, func_apm):
+ func_apm.return_value = False
+ self.update_common_config(
+ rum_key='',
+ application='app',
+ environment='env'
+ )
+
+ result = stackify.rum.insert_rum_script()
+ assert not result
+ assert result is ''
+
+ self.reset_common_config()
+
+ @mock.patch('stackify.rum.is_apm_installed')
+ def test_default_insert_rum_script_no_details(self, func_apm):
+ func_apm.return_value = False
+ self.update_common_config()
+
+ result = stackify.rum.insert_rum_script()
+ assert not result
+ assert result is ''
+
+ self.reset_common_config()
+
+ @mock.patch('stackify.rum.is_apm_installed')
+ @mock.patch('stackify.rum.get_reporting_url')
+ @mock.patch('stackify.rum.get_transaction_id')
+ def test_default_insert_rum_script_from_api(self, func, func_reporting_url, func_apm):
+ func.return_value = '123'
+ func_apm.return_value = False
+ func_reporting_url.return_value = 'test reporting url'
+ self.create_config(
+ rum_key='asd1',
+ application='app1',
+ environment='env1'
+ )
+ rum_settings = {
+ "ID": '123',
+ "Name": 'YXBwMQ==',
+ "Env": 'ZW52MQ==',
+ "Trans": 'dGVzdCByZXBvcnRpbmcgdXJs'
+ }
+ result = stackify.rum.insert_rum_script()
+ self.reset_common_config()
+ assert result == ''.format(json.dumps(rum_settings))
+
+ @mock.patch('stackify.rum.is_apm_installed')
+ def test_default_insert_rum_script_no_key_from_api(self, func_apm):
+ func_apm.return_value = False
+ self.create_config(
+ rum_key=None,
+ application='app2',
+ environment='env2'
+ )
+
+ result = stackify.rum.insert_rum_script()
+ self.reset_common_config()
+
+ assert not result
+ assert result is ''
+
+ @mock.patch('stackify.rum.is_apm_installed')
+ def test_default_insert_rum_script_no_details_from_api(self, func_apm):
+ func_apm.return_value = False
+ self.create_config(
+ application=None,
+ environment=None,
+ rum_key=None
+ )
+
+ result = stackify.rum.insert_rum_script()
+ self.reset_common_config()
+
+ assert not result
+ assert result is ''
+
+ def update_common_config(self, rum_key=None, rum_script_url=None, application=None, environment=None):
+ self.config_rum_key = stackify.config.rum_key
+ self.config_rum_script_url = stackify.config.rum_script_url
+ self.config_application = stackify.config.application
+ self.config_environment = stackify.config.environment
+
+ if rum_key is not None:
+ stackify.config.rum_key = rum_key
+ if rum_script_url is not None:
+ stackify.config.rum_script_url = rum_script_url
+ if application is not None:
+ stackify.config.application = application
+ if environment is not None:
+ stackify.config.environment = environment
+
+ def reset_common_config(self):
+ stackify.config.rum_key = self.config_rum_key
+ stackify.config.rum_script_url = self.config_rum_script_url
+ stackify.config.application = self.config_application
+ stackify.config.environment = self.config_environment
+
+ def create_config(self, **kwargs):
+ return ApiConfiguration(
+ application=kwargs['application'],
+ environment=kwargs['environment'],
+ api_key='test_apikey',
+ api_url='test_apiurl',
+ rum_script_url=arg_or_env('rum_script_url', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_SCRIPT_URL'),
+ rum_key=arg_or_env('rum_key', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_KEY')
+ )
+
+
+class RumConfigurationTest(ClearEnvTest):
+ '''
+ Test the logger init functionality
+ '''
+
+ def setUp(self):
+ super(RumConfigurationTest, self).setUp()
+ self.config_rum_key = stackify.config.rum_key
+ self.config_rum_script_url = stackify.config.rum_script_url
+ self.config_application = stackify.config.application
+ self.config_environment = stackify.config.environment
+
+ def tearDown(self):
+ super(RumConfigurationTest, self).tearDown()
+
+ def test_rum_script_url_valid(self):
+ os.environ["RETRACE_RUM_SCRIPT_URL"] = 'https://test.com/test.js'
+ config = self.create_config()
+ assert config.rum_script_url == 'https://test.com/test.js'
+ del os.environ["RETRACE_RUM_SCRIPT_URL"]
+ self.reset_common_config()
+
+ @mock.patch('logging.Logger.exception')
+ def test_rum_script_url_invalid(self, func=None):
+ os.environ["RETRACE_RUM_SCRIPT_URL"] = 'asd'
+ config = self.create_config()
+ assert config.rum_script_url == 'https://stckjs.stackify.com/stckjs.js' # Default
+ del os.environ["RETRACE_RUM_SCRIPT_URL"]
+ self.reset_common_config()
+ func.assert_called_with('https://stckjs.stackify.com/stckjs.js does not match pattern ^[A-Za-z0-9_-]+$')
+
+ def test_rum_key_valid(self):
+ os.environ["RETRACE_RUM_KEY"] = 'TEST123-_'
+ config = self.create_config()
+ assert config.rum_key == 'TEST123-_'
+ del os.environ["RETRACE_RUM_KEY"]
+ self.reset_common_config()
+
+ @mock.patch('logging.Logger.exception')
+ def test_rum_key_invalid(self, func=None):
+ os.environ["RETRACE_RUM_KEY"] = 'asd`1!'
+ config = self.create_config()
+ assert config.rum_key == '' # Default
+ del os.environ["RETRACE_RUM_KEY"]
+ self.reset_common_config()
+ func.assert_called_with('asd`1! does not match pattern ^[A-Za-z0-9_-]+$')
+
+ def create_config(self, **kwargs):
+ return ApiConfiguration(
+ application='test_appname',
+ environment='test_environment',
+ api_key='test_apikey',
+ api_url='test_apiurl',
+ rum_script_url=arg_or_env('rum_script_url', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_SCRIPT_URL'),
+ rum_key=arg_or_env('rum_key', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_KEY')
+ )
+
+ def reset_common_config(self):
+ stackify.config.rum_key = self.config_rum_key
+ stackify.config.rum_script_url = self.config_rum_script_url
+ stackify.config.application = self.config_application
+ stackify.config.environment = self.config_environment
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 70e4b78..2ec637c 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -9,6 +9,9 @@
import stackify
import logging
+from stackify.utils import ConfigError
+from stackify.utils import RegexValidator
+
class TestInit(ClearEnvTest):
'''
@@ -91,6 +94,28 @@ def test_utils_data_to_json_dummy_request(self):
self.assertTrue(substring in result)
+class RegexValidatorTest(ClearEnvTest):
+
+ def test_should_return_correct_value(self):
+ regex = "^[a-zA-Z0-9 _-]+$"
+ value = 'some_value'
+ _validate = RegexValidator(regex)
+
+ validated_value = _validate(value, 'SOME_KEY')
+
+ assert validated_value == value
+
+ def test_should_raise_exception(self):
+ regex = "^[a-zA-Z0-9 _-]+$"
+ value = '#$%^'
+ _validate = RegexValidator(regex)
+
+ with self.assertRaises(ConfigError) as context:
+ _validate(value, 'SOME_KEY')
+
+ assert 'does not match pattern' in context.exception.args[0]
+
+
class Dummy(object):
pass