Repository: elastic/apm-agent-python

Issue:
[META 781] send activation_method only for 8.7.1+
See meta issue for the description and details:
- Meta issue: https://github.com/elastic/apm/issues/781



Discussion:

Resolving patch:
diff --git a/elasticapm/base.py b/elasticapm/base.py
--- a/elasticapm/base.py
+++ b/elasticapm/base.py
@@ -107,6 +107,7 @@ def __init__(self, config=None, **inline):
         self.processors = []
         self.filter_exception_types_dict = {}
         self._service_info = None
+        self._server_version = None
         # setting server_version here is mainly used for testing
         self.server_version = inline.pop("server_version", None)
         self.activation_method = elasticapm._activation_method
@@ -355,7 +356,7 @@ def get_service_info(self):
                 "version": keyword_field(runtime_version),
             },
         }
-        if self.activation_method:
+        if self.activation_method and self.check_server_version(gte=(8, 7, 1)):
             result["agent"]["activation_method"] = self.activation_method
         if self.config.framework_name:
             result["framework"] = {
@@ -680,7 +681,11 @@ def check_server_version(
         if not self.server_version:
             return True
         gte = gte or (0,)
+        if len(gte) < 3:
+            gte = gte + (0,) * (3 - len(gte))
         lte = lte or (2**32,)  # let's assume APM Server version will never be greater than 2^32
+        if len(lte) < 3:
+            lte = lte + (0,) * (3 - len(lte))
         return bool(gte <= self.server_version <= lte)
 
     @property
@@ -688,6 +693,17 @@ def _metrics(self):
         warnings.warn(DeprecationWarning("Use `client.metrics` instead"))
         return self.metrics
 
+    @property
+    def server_version(self):
+        return self._server_version
+
+    @server_version.setter
+    def server_version(self, new_version):
+        if new_version and len(new_version) < 3:
+            self.logger.debug("APM Server version is too short, padding with zeros")
+            new_version = new_version + (0,) * (3 - len(new_version))
+        self._server_version = new_version
+
 
 class DummyClient(Client):
     """Sends messages into an empty void"""


Test patch:
diff --git a/tests/client/client_tests.py b/tests/client/client_tests.py
--- a/tests/client/client_tests.py
+++ b/tests/client/client_tests.py
@@ -508,7 +508,7 @@ def test_check_server_version(elasticapm_client):
     assert elasticapm_client.check_server_version(gte=(100, 5, 10))
     assert elasticapm_client.check_server_version(lte=(100, 5, 10))
 
-    elasticapm_client.server_version = (7, 15)
+    elasticapm_client.server_version = (7, 15, 0)
     assert elasticapm_client.check_server_version(gte=(7,))
     assert not elasticapm_client.check_server_version(gte=(8,))
     assert not elasticapm_client.check_server_version(lte=(7,))
diff --git a/tests/client/dropped_spans_tests.py b/tests/client/dropped_spans_tests.py
--- a/tests/client/dropped_spans_tests.py
+++ b/tests/client/dropped_spans_tests.py
@@ -113,7 +113,9 @@ def test_transaction_max_span_dropped_statistics(elasticapm_client):
         assert entry["service_target_name"] == "foo"
 
 
-@pytest.mark.parametrize("elasticapm_client", [{"transaction_max_spans": 1, "server_version": (7, 15)}], indirect=True)
+@pytest.mark.parametrize(
+    "elasticapm_client", [{"transaction_max_spans": 1, "server_version": (7, 15, 0)}], indirect=True
+)
 def test_transaction_max_span_dropped_statistics_not_collected_for_incompatible_server(elasticapm_client):
     elasticapm_client.begin_transaction("test_type")
     with elasticapm.capture_span("not_dropped"):
diff --git a/tests/client/transaction_tests.py b/tests/client/transaction_tests.py
--- a/tests/client/transaction_tests.py
+++ b/tests/client/transaction_tests.py
@@ -38,7 +38,9 @@
 from elasticapm.utils.disttracing import TraceParent
 
 
-@pytest.mark.parametrize("elasticapm_client", [{"server_version": (7, 15)}, {"server_version": (7, 16)}], indirect=True)
+@pytest.mark.parametrize(
+    "elasticapm_client", [{"server_version": (7, 15, 0)}, {"server_version": (7, 16, 0)}], indirect=True
+)
 def test_transaction_span(elasticapm_client):
     elasticapm_client.begin_transaction("test")
     with elasticapm.capture_span("test", extra={"a": "b"}):
@@ -156,8 +158,8 @@ def test_collect_source_transactions(elasticapm_client):
 @pytest.mark.parametrize(
     "elasticapm_client",
     [
-        {"transaction_sample_rate": 0.4, "server_version": (7, 14)},
-        {"transaction_sample_rate": 0.4, "server_version": (8, 0)},
+        {"transaction_sample_rate": 0.4, "server_version": (7, 14, 0)},
+        {"transaction_sample_rate": 0.4, "server_version": (8, 0, 0)},
     ],
     indirect=True,
 )
@@ -175,7 +177,7 @@ def test_transaction_sampling(elasticapm_client, not_so_random):
 
     # seed is fixed by not_so_random fixture
     assert len([t for t in transactions if t["sampled"]]) == 3
-    if elasticapm_client.server_version < (8, 0):
+    if elasticapm_client.server_version < (8, 0, 0):
         assert len(transactions) == 10
     else:
         assert len(transactions) == 3
diff --git a/tests/contrib/django/django_tests.py b/tests/contrib/django/django_tests.py
--- a/tests/contrib/django/django_tests.py
+++ b/tests/contrib/django/django_tests.py
@@ -1633,7 +1633,7 @@ def test_transaction_name_from_route_doesnt_have_effect_in_older_django(client,
 
 
 @pytest.mark.parametrize(
-    "django_elasticapm_client", [{"server_version": (7, 14)}], indirect=True
+    "django_elasticapm_client", [{"server_version": (7, 14, 0)}], indirect=True
 )  # unsampled transactions are dropped with server 8.0+
 def test_outcome_is_set_for_unsampled_transactions(django_elasticapm_client, client):
     with override_settings(
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
@@ -343,7 +343,7 @@ def test_labels_dedot(elasticapm_client):
 
 
 @pytest.mark.parametrize(
-    "elasticapm_client", [{"server_version": (7, 14)}], indirect=True
+    "elasticapm_client", [{"server_version": (7, 14, 0)}], indirect=True
 )  # unsampled transactions are dropped with server 8.0+
 def test_dedot_is_not_run_when_unsampled(elasticapm_client):
     for sampled in (True, False):
@@ -425,7 +425,7 @@ def test_set_user_context_merge(elasticapm_client):
 
 
 @pytest.mark.parametrize(
-    "elasticapm_client", [{"server_version": (7, 14)}], indirect=True
+    "elasticapm_client", [{"server_version": (7, 14, 0)}], indirect=True
 )  # unsampled transactions are dropped with server 8.0+
 def test_callable_context_ignored_when_not_sampled(elasticapm_client):
     callable_data = mock.Mock()
diff --git a/tests/transports/test_urllib3.py b/tests/transports/test_urllib3.py
--- a/tests/transports/test_urllib3.py
+++ b/tests/transports/test_urllib3.py
@@ -50,7 +50,7 @@
 
 @pytest.mark.flaky(reruns=3)  # test is flaky on Windows
 def test_send(waiting_httpserver, elasticapm_client):
-    elasticapm_client.server_version = (8, 0)  # avoid making server_info request
+    elasticapm_client.server_version = (8, 0, 0)  # avoid making server_info request
     waiting_httpserver.serve_content(code=202, content="", headers={"Location": "http://example.com/foo"})
     transport = Transport(
         waiting_httpserver.url, client=elasticapm_client, headers=elasticapm_client._transport._headers
@@ -70,7 +70,7 @@ def test_send(waiting_httpserver, elasticapm_client):
 
 @mock.patch("urllib3.poolmanager.PoolManager.urlopen")
 def test_timeout(mock_urlopen, elasticapm_client):
-    elasticapm_client.server_version = (8, 0)  # avoid making server_info request
+    elasticapm_client.server_version = (8, 0, 0)  # avoid making server_info request
     transport = Transport("http://localhost", timeout=5, client=elasticapm_client)
     transport.start_thread()
     mock_urlopen.side_effect = MaxRetryError(None, None, reason=TimeoutError())
@@ -84,7 +84,7 @@ def test_timeout(mock_urlopen, elasticapm_client):
 
 @pytest.mark.flaky(reruns=3)  # test is flaky on Windows
 def test_http_error(waiting_httpserver, elasticapm_client):
-    elasticapm_client.server_version = (8, 0)  # avoid making server_info request
+    elasticapm_client.server_version = (8, 0, 0)  # avoid making server_info request
     waiting_httpserver.serve_content(code=418, content="I'm a teapot")
     transport = Transport(waiting_httpserver.url, client=elasticapm_client)
     transport.start_thread()
@@ -100,7 +100,7 @@ def test_http_error(waiting_httpserver, elasticapm_client):
 @mock.patch("urllib3.poolmanager.PoolManager.urlopen")
 def test_generic_error(mock_urlopen, elasticapm_client):
     url, status, message, body = ("http://localhost:9999", 418, "I'm a teapot", "Nothing")
-    elasticapm_client.server_version = (8, 0)  # avoid making server_info request
+    elasticapm_client.server_version = (8, 0, 0)  # avoid making server_info request
     transport = Transport(url, client=elasticapm_client)
     transport.start_thread()
     mock_urlopen.side_effect = Exception("Oopsie")
@@ -156,7 +156,7 @@ def test_header_encodings(elasticapm_client):
     encoded bytestring, and explodes.
     """
     headers = {str("X"): str("V")}
-    elasticapm_client.server_version = (8, 0)  # avoid making server_info request
+    elasticapm_client.server_version = (8, 0, 0)  # avoid making server_info request
     transport = Transport("http://localhost:9999", headers=headers, client=elasticapm_client)
     transport.start_thread()
     try:
@@ -406,8 +406,8 @@ def test_use_certifi(elasticapm_client):
             (1, 2, "3alpha1"),
         ),
         (
-            "",
-            (),
+            "1.2",
+            (1, 2),
         ),
     ],
 )
@@ -415,17 +415,42 @@ def test_server_version_to_tuple(version, expected):
     assert version_string_to_tuple(version) == expected
 
 
-def test_fetch_server_info(waiting_httpserver, elasticapm_client):
+@pytest.mark.parametrize(
+    "version,expected",
+    [
+        (
+            "1.2.3",
+            (1, 2, 3),
+        ),
+        (
+            "1.2.3-alpha1",
+            (1, 2, 3, "alpha1"),
+        ),
+        (
+            "1.2.3alpha1",
+            (1, 2, "3alpha1"),
+        ),
+        (
+            "1.2",
+            (1, 2, 0),
+        ),
+        (
+            "8.0.0-alpha1",
+            (8, 0, 0, "alpha1"),
+        ),
+    ],
+)
+def test_fetch_server_info(waiting_httpserver, elasticapm_client, version, expected):
     waiting_httpserver.serve_content(
         code=200,
-        content=b'{"version": "8.0.0-alpha1"}',
+        content=f'{{"version": "{version}"}}'.encode("utf-8"),
     )
     url = waiting_httpserver.url
     transport = Transport(
         url + "/" + constants.EVENTS_API_PATH, client=elasticapm_client, headers=elasticapm_client._transport._headers
     )
     transport.fetch_server_info()
-    assert elasticapm_client.server_version == (8, 0, 0, "alpha1")
+    assert elasticapm_client.server_version == expected
     request_headers = waiting_httpserver.requests[0].headers
     assert request_headers["User-Agent"].startswith("apm-agent-python/")
     assert "Authorization" in request_headers