Repository: scoutapp/scout_apm_python
Pull request #141 (2018-12-16 17:13:17)

Accept non-ASCII bytestrings in public APIs.
This is mainly useful for Python 2.7 users that use a language other than English and don't take advantage of Python's unicode support — hopefully a small group of users.


Patch:
diff --git a/src/scout_apm/api/__init__.py b/src/scout_apm/api/__init__.py
index b2f1b60..467c7c7 100644
--- a/src/scout_apm/api/__init__.py
+++ b/src/scout_apm/api/__init__.py
@@ -29,6 +29,19 @@ else:
     from contextlib import ContextDecorator
 
 
+text_type = str if sys.version_info[0] >= 3 else unicode  # noqa: F821
+
+
+def text(value, encoding="utf-8", errors="strict"):
+    """Convert a value to str on Python 3 and unicode on Python 2."""
+    if isinstance(value, text_type):
+        return value
+    elif isinstance(value, bytes):
+        return text_type(value, encoding, errors)
+    else:
+        return text_type(value)
+
+
 class Context(ScoutContext):
     pass
 
@@ -43,7 +56,7 @@ def install(*args, **kwargs):
 
 class instrument(ContextDecorator):
     def __init__(self, operation, kind="Custom", tags={}):
-        self.operation = kind + "/" + operation
+        self.operation = text(kind) + "/" + text(operation)
         self.tags = tags
 
     def __enter__(self):
@@ -71,12 +84,12 @@ class Transaction(ContextDecorator):
     """
 
     def __init__(self, name, tags={}):
-        self.name = name
+        self.name = text(name)
         self.tags = tags
 
     @classmethod
     def start(cls, kind, name, tags={}):
-        operation = kind + "/" + name
+        operation = text(kind) + "/" + text(name)
 
         tr = TrackedRequest.instance()
         tr.mark_real_request()
@@ -106,7 +119,7 @@ class Transaction(ContextDecorator):
 class WebTransaction(Transaction):
     @classmethod
     def start(cls, name, tags={}):
-        Transaction.start("Controller", name, tags)
+        Transaction.start("Controller", text(name), tags)
 
     def __enter__(self):
         Transaction.start("Controller", self.name, self.tags)
@@ -115,7 +128,7 @@ class WebTransaction(Transaction):
 class BackgroundTransaction(Transaction):
     @classmethod
     def start(cls, name, tags={}):
-        Transaction.start("Job", name, tags)
+        Transaction.start("Job", text(name), tags)
 
     def __enter__(self):
         Transaction.start("Job", self.name, self.tags)


Test patch:
diff --git a/tests/integration/test_api.py b/tests/integration/test_api.py
index a01575c..819049b 100644
--- a/tests/integration/test_api.py
+++ b/tests/integration/test_api.py
@@ -1,3 +1,5 @@
+# coding: utf-8
+
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 from scout_apm.api import (
@@ -85,6 +87,39 @@ def test_instrument_decorator_default_tags():
     assert span.tags["x"] == 99
 
 
+def test_instrument_non_ascii_params():
+    # Save TR here, so it doesn't disappear on us when span finishes
+    tr = TrackedRequest.instance()
+
+    @instrument(operation="Faire le café", kind="Personnalisé")
+    def make_coffee():
+        pass
+
+    make_coffee()
+
+    span = tr.complete_spans[-1]
+    assert span.operation == "Personnalisé/Faire le café"
+
+
+def test_instrument_non_ascii_bytes_params():
+    # Save TR here, so it doesn't disappear on us when span finishes
+    tr = TrackedRequest.instance()
+
+    # On Python 2, user code that doesn't enable unicode_literals may contain
+    # non-ASCII bystrings. For writing a test that works across Python versions,
+    # the easiest is to create bytestrings by encoding unicode strings.
+    @instrument(
+        operation="Faire le café".encode("utf-8"), kind="Personnalisé".encode("utf-8")
+    )
+    def make_coffee():
+        pass
+
+    make_coffee()
+
+    span = tr.complete_spans[-1]
+    assert span.operation == "Personnalisé/Faire le café"
+
+
 def test_web_transaction_start_stop():
     tr = TrackedRequest.instance()
 
@@ -120,6 +155,32 @@ def test_web_transaction_decorator():
     assert span.operation == "Controller/Bar"
 
 
+def test_web_transaction_non_ascii_params():
+    tr = TrackedRequest.instance()
+
+    @WebTransaction("Acheter du café")
+    def buy_coffee():
+        pass
+
+    buy_coffee()
+
+    span = tr.complete_spans[-1]
+    assert span.operation == "Controller/Acheter du café"
+
+
+def test_web_transaction_non_ascii_bytes_params():
+    tr = TrackedRequest.instance()
+
+    @WebTransaction("Acheter du café".encode("utf-8"))
+    def buy_coffee():
+        pass
+
+    buy_coffee()
+
+    span = tr.complete_spans[-1]
+    assert span.operation == "Controller/Acheter du café"
+
+
 def test_background_transaction_start_stop():
     tr = TrackedRequest.instance()
 
@@ -155,6 +216,32 @@ def test_background_transaction_decorator():
     assert span.operation == "Job/Bar"
 
 
+def test_background_transaction_non_ascii_params():
+    tr = TrackedRequest.instance()
+
+    @BackgroundTransaction("Acheter du café")
+    def buy_coffee():
+        pass
+
+    buy_coffee()
+
+    span = tr.complete_spans[-1]
+    assert span.operation == "Job/Acheter du café"
+
+
+def test_background_transaction_non_ascii_bytes_params():
+    tr = TrackedRequest.instance()
+
+    @BackgroundTransaction("Acheter du café".encode("utf-8"))
+    def buy_coffee():
+        pass
+
+    buy_coffee()
+
+    span = tr.complete_spans[-1]
+    assert span.operation == "Job/Acheter du café"
+
+
 def test_context():
     # Save TR here, so it doesn't disappear on us when span finishes
     tr = TrackedRequest.instance()