Repository: elastic/apm-agent-python

Issue:
Force transaction name in `set_transaction_name` to string
**Describe the bug**: 

When providing a non-string value to `elasticapm.set_transaction_name()`, APM Server discards the transaction.

To avoid this, we should try and force the value to a string, raising a warning if it isn't possible.

**To Reproduce**

1. start a transaction
2. call `elasticapm.set_transaction_name(["a list"])`
3. transaction fails schema validation


**Environment (please complete the following information)**
- OS: [e.g. Linux] Linux
- Python version: 3.9
- Framework and version [e.g. Django 2.1]:
- APM Server version: 7.13
- Agent version: 6.2.2




Discussion:

Resolving patch:
diff --git a/elasticapm/traces.py b/elasticapm/traces.py
--- a/elasticapm/traces.py
+++ b/elasticapm/traces.py
@@ -793,7 +793,7 @@ def end_transaction(self, result=None, transaction_name=None, duration=None):
         transaction = execution_context.get_transaction(clear=True)
         if transaction:
             if transaction.name is None:
-                transaction.name = transaction_name if transaction_name is not None else ""
+                transaction.name = str(transaction_name) if transaction_name is not None else ""
             transaction.end(duration=duration)
             if self._should_ignore(transaction.name):
                 return
@@ -925,7 +925,7 @@ def label(**labels):
         transaction.label(**labels)
 
 
-def set_transaction_name(name, override=True):
+def set_transaction_name(name: str, override: bool = True) -> None:
     """
     Sets the name of the transaction
 
@@ -937,7 +937,7 @@ def set_transaction_name(name, override=True):
     if not transaction:
         return
     if transaction.name is None or override:
-        transaction.name = name
+        transaction.name = str(name)
 
 
 def set_transaction_result(result, override=True):


Test patch:
diff --git a/tests/instrumentation/transactions_store_tests.py b/tests/instrumentation/transactions_store_tests.py
--- a/tests/instrumentation/transactions_store_tests.py
+++ b/tests/instrumentation/transactions_store_tests.py
@@ -346,19 +346,26 @@ def test_dedot_is_not_run_when_unsampled(elasticapm_client):
     assert "context" not in unsampled_transaction
 
 
-def test_set_transaction_name(elasticapm_client):
+@pytest.mark.parametrize(
+    "name1,name2,expected1,expected2",
+    [
+        ("test_name", "another_name", "test_name", "another_name"),
+        (["test_name"], {"another_name": "foo"}, "['test_name']", "{'another_name': 'foo'}"),
+    ],
+)
+def test_set_transaction_name(elasticapm_client, name1, name2, expected1, expected2):
     elasticapm_client.begin_transaction("test")
-    elasticapm_client.end_transaction("test_name", 200)
+    elasticapm_client.end_transaction(name1, 200)
 
     elasticapm_client.begin_transaction("test")
 
-    elasticapm.set_transaction_name("another_name")
+    elasticapm.set_transaction_name(name2)
 
-    elasticapm_client.end_transaction("test_name", 200)
+    elasticapm_client.end_transaction(name1, 200)
 
     transactions = elasticapm_client.events[TRANSACTION]
-    assert transactions[0]["name"] == "test_name"
-    assert transactions[1]["name"] == "another_name"
+    assert transactions[0]["name"] == expected1
+    assert transactions[1]["name"] == expected2
 
 
 def test_set_transaction_custom_data(elasticapm_client):