nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: scoutapp/scout_apm_python
Pull request #310 (2019-10-28 21:06:29)

Change Windows check
I saw that flake8's `is_windows()` check prefers `os.name`, and it seems simpler.


Patch:
diff --git a/src/scout_apm/core/__init__.py b/src/scout_apm/core/__init__.py
index 1366a9e..123a33c 100644
--- a/src/scout_apm/core/__init__.py
+++ b/src/scout_apm/core/__init__.py
@@ -2,8 +2,7 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import logging
-import sys
-from os import getpid
+import os
 
 from kwargs_only import kwargs_only
 
@@ -24,14 +23,16 @@ def install(config=None):
         scout_config.set(**config)
     context = AgentContext.build(config=scout_config)
 
-    if sys.platform == "win32":
-        logger.info("APM Not Launching on PID: %s - Windows is not supported", getpid())
+    if os.name == "nt":
+        logger.info(
+            "APM Not Launching on PID: %s - Windows is not supported", os.getpid()
+        )
         return False
 
     if not context.config.value("monitor"):
         logger.info(
             "APM Not Launching on PID: %s - Configuration 'monitor' is not true",
-            getpid(),
+            os.getpid(),
         )
         return False
 
@@ -39,7 +40,7 @@ def install(config=None):
 
     objtrace.enable()
 
-    logger.debug("APM Launching on PID: %s", getpid())
+    logger.debug("APM Launching on PID: %s", os.getpid())
     launched = CoreAgentManager().launch()
 
     AppMetadata.report()


Test patch:
diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py
index bcfe54b..3cdda80 100644
--- a/tests/unit/test_core.py
+++ b/tests/unit/test_core.py
@@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 
 import logging
 import os
-import sys
 
 from scout_apm.api import Config
 from scout_apm.core import install
@@ -12,7 +11,7 @@ from tests.compat import mock
 
 
 def test_install_fail_windows(caplog):
-    with mock.patch.object(sys, "platform", new="win32"):
+    with mock.patch.object(os, "name", new="nt"):
         installed = install()
 
     assert installed is False
Repository: scoutapp/scout_apm_python
Pull request #369 (2019-11-07 10:01:44)

Ensure install() is called for Huey
Ensure `scout_apm.core.install()` is called in the Huey integration, and don't monitor if install fails.


Patch:
diff --git a/src/scout_apm/huey.py b/src/scout_apm/huey.py
index c33f3d8..382d577 100644
--- a/src/scout_apm/huey.py
+++ b/src/scout_apm/huey.py
@@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 from huey.exceptions import RetryTask, TaskLockedException
 from huey.signals import SIGNAL_CANCELED
 
+import scout_apm.core
 from scout_apm.core.tracked_request import TrackedRequest
 
 # Because neither hooks nor signals are called in *all* cases, we need to use
@@ -12,9 +13,11 @@ from scout_apm.core.tracked_request import TrackedRequest
 
 
 def attach_scout(huey):
-    huey.pre_execute()(scout_on_pre_execute)
-    huey.post_execute()(scout_on_post_execute)
-    huey.signal(SIGNAL_CANCELED)(scout_on_cancelled)
+    installed = scout_apm.core.install()
+    if installed:
+        huey.pre_execute()(scout_on_pre_execute)
+        huey.post_execute()(scout_on_post_execute)
+        huey.signal(SIGNAL_CANCELED)(scout_on_cancelled)
 
 
 def scout_on_pre_execute(task):


Test patch:
diff --git a/tests/integration/test_huey.py b/tests/integration/test_huey.py
index f0eed11..0e81e41 100644
--- a/tests/integration/test_huey.py
+++ b/tests/integration/test_huey.py
@@ -124,3 +124,12 @@ def test_locked(tracked_requests):
 
     assert excinfo.value.metadata["error"].startswith("TaskLockedException")
     assert tracked_requests == []
+
+
+def test_no_monitor(tracked_requests):
+    with app_with_scout(scout_config={"monitor": False}) as app:
+        result = app.hello()
+        value = result(blocking=True, timeout=1)
+
+    assert value == "Hello World!"
+    assert tracked_requests == []
Repository: scoutapp/scout_apm_python
Pull request #385 (2019-11-23 11:33:06)

Tag Bottle 5XX responses as errors
Ref #383.


Patch:
diff --git a/src/scout_apm/bottle.py b/src/scout_apm/bottle.py
index 2b92462..44e73b8 100644
--- a/src/scout_apm/bottle.py
+++ b/src/scout_apm/bottle.py
@@ -2,7 +2,7 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import wrapt
-from bottle import request
+from bottle import request, response
 
 import scout_apm.core
 from scout_apm.core.config import scout_config
@@ -82,9 +82,13 @@ def wrap_callback(wrapped, instance, args, kwargs):
         track_amazon_request_queue_time(amazon_queue_time, tracked_request)
 
     try:
-        return wrapped(*args, **kwargs)
+        value = wrapped(*args, **kwargs)
     except Exception:
         tracked_request.tag("error", "true")
         raise
+    else:
+        if 500 <= response.status_code <= 599:
+            tracked_request.tag("error", "true")
+        return value
     finally:
         tracked_request.stop_span()


Test patch:
diff --git a/tests/integration/test_bottle.py b/tests/integration/test_bottle.py
index 5aa329a..c44579d 100644
--- a/tests/integration/test_bottle.py
+++ b/tests/integration/test_bottle.py
@@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 import datetime as dt
 from contextlib import contextmanager
 
-from bottle import Bottle
+from bottle import Bottle, response
 from webtest import TestApp
 
 from scout_apm.api import Config
@@ -45,6 +45,11 @@ def app_with_scout(config=None, catchall=False):
     def crash():
         raise ValueError("BØØM!")  # non-ASCII
 
+    @app.route("/return-error/")
+    def return_error():
+        response.status = 503
+        return "Something went wrong"
+
     @app.route("/named/", name="named_route")
     def named():
         return "Response from a named route."
@@ -181,6 +186,19 @@ def test_server_error(tracked_requests):
     assert span.operation == "Controller/crash/"
 
 
+def test_return_error(tracked_requests):
+    with app_with_scout() as app:
+        response = TestApp(app).get("/return-error/", expect_errors=True)
+
+    assert response.status_int == 503
+    assert len(tracked_requests) == 1
+    tracked_request = tracked_requests[0]
+    assert tracked_request.tags["error"] == "true"
+    assert len(tracked_request.complete_spans) == 1
+    span = tracked_requests[0].complete_spans[0]
+    assert span.operation == "Controller/return-error/"
+
+
 def test_named(tracked_requests):
     with app_with_scout() as app:
         response = TestApp(app).get("/named/")
Repository: scoutapp/scout_apm_python
Pull request #389 (2019-11-23 17:31:34)

Tag Pyramid 5XX responses as errors
Ref #383.


Patch:
diff --git a/src/scout_apm/pyramid.py b/src/scout_apm/pyramid.py
index ad765bb..218447d 100644
--- a/src/scout_apm/pyramid.py
+++ b/src/scout_apm/pyramid.py
@@ -66,8 +66,8 @@ def instruments(handler, registry):
                 try:
                     response = handler(request)
                 finally:
-                    # Routing further down the call chain. So time it starting
-                    # above, but only name it if it gets a name
+                    # Routing lives further down the call chain. So time it
+                    # starting above, but only set the name if it gets a name
                     if request.matched_route is not None:
                         tracked_request.is_real_request = True
                         span.operation = "Controller/" + request.matched_route.name
@@ -75,6 +75,8 @@ def instruments(handler, registry):
                 tracked_request.tag("error", "true")
                 raise
 
+            if 500 <= response.status_code <= 599:
+                tracked_request.tag("error", "true")
         finally:
             tracked_request.stop_span()
 


Test patch:
diff --git a/tests/integration/test_pyramid.py b/tests/integration/test_pyramid.py
index 012dde4..3c28b76 100644
--- a/tests/integration/test_pyramid.py
+++ b/tests/integration/test_pyramid.py
@@ -40,6 +40,9 @@ def app_with_scout(config=None):
     def crash(request):
         raise ValueError("BØØM!")  # non-ASCII
 
+    def return_error(request):
+        return Response("Something went wrong", status=503)
+
     with Configurator() as configurator:
         configurator.add_route("home", "/")
         configurator.add_view(home, route_name="home", request_method="GET")
@@ -47,6 +50,8 @@ def app_with_scout(config=None):
         configurator.add_view(hello, route_name="hello")
         configurator.add_route("crash", "/crash/")
         configurator.add_view(crash, route_name="crash")
+        configurator.add_route("return_error", "/return-error/")
+        configurator.add_view(return_error, route_name="return_error")
 
         # Setup according to https://docs.scoutapm.com/#pyramid
         configurator.add_settings(**config)
@@ -180,6 +185,21 @@ def test_server_error(tracked_requests):
     assert span.operation == "Controller/crash"
 
 
+def test_return_error(tracked_requests):
+    with app_with_scout() as app:
+        response = TestApp(app).get("/return-error/", expect_errors=True)
+
+    assert response.status_int == 503
+    assert response.text == "Something went wrong"
+    assert len(tracked_requests) == 1
+    tracked_request = tracked_requests[0]
+    assert tracked_request.tags["path"] == "/return-error/"
+    assert tracked_request.tags["error"] == "true"
+    assert len(tracked_request.complete_spans) == 1
+    span = tracked_request.complete_spans[0]
+    assert span.operation == "Controller/return_error"
+
+
 def test_no_monitor(tracked_requests):
     # With an empty config, "SCOUT_MONITOR" defaults to False.
     with app_with_scout({}) as app:
Repository: scoutapp/scout_apm_python
Pull request #470 (2020-02-05 17:52:33)

Extract decorator unwrapping to a separate function
Refactor this code added in #466 and test it separately.


Patch:
diff --git a/src/scout_apm/compat.py b/src/scout_apm/compat.py
index 050cf6a..ad7a83d 100644
--- a/src/scout_apm/compat.py
+++ b/src/scout_apm/compat.py
@@ -75,6 +75,17 @@ else:
         return inspect.getargspec(func).args
 
 
+def unwrap_decorators(func):
+    unwrapped = func
+    while True:
+        # N.B. only some decorators set __wrapped__ on Python 2.7
+        try:
+            unwrapped = unwrapped.__wrapped__
+        except AttributeError:
+            break
+    return unwrapped
+
+
 def kwargs_only(func):
     """
     Source: https://pypi.org/project/kwargs-only/
diff --git a/src/scout_apm/instruments/elasticsearch.py b/src/scout_apm/instruments/elasticsearch.py
index 8bfdbf4..20f21d2 100644
--- a/src/scout_apm/instruments/elasticsearch.py
+++ b/src/scout_apm/instruments/elasticsearch.py
@@ -6,7 +6,7 @@ from collections import namedtuple
 
 import wrapt
 
-from scout_apm.compat import get_pos_args
+from scout_apm.compat import get_pos_args, unwrap_decorators
 from scout_apm.core.tracked_request import TrackedRequest
 
 try:
@@ -104,13 +104,7 @@ def wrap_client_index_method(wrapped, instance, args, kwargs):
     if "index" in kwargs:
         index = kwargs["index"]
     else:
-        unwrapped = wrapped
-        while True:
-            try:
-                unwrapped = unwrapped.__wrapped__
-            except AttributeError:
-                break
-
+        unwrapped = unwrap_decorators(wrapped)
         pos_args = get_pos_args(unwrapped)
         try:
             index_index = pos_args.index("index")


Test patch:
diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py
index 79d9db3..67ae0bc 100644
--- a/tests/unit/test_compat.py
+++ b/tests/unit/test_compat.py
@@ -2,10 +2,18 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import datetime as dt
+import functools
+import sys
 
 import pytest
 
-from scout_apm.compat import ContextDecorator, datetime_to_timestamp, get_pos_args, text
+from scout_apm.compat import (
+    ContextDecorator,
+    datetime_to_timestamp,
+    get_pos_args,
+    text,
+    unwrap_decorators,
+)
 
 
 def test_context_decorator():
@@ -62,3 +70,24 @@ def test_get_pos_args_kwargs():
         pass
 
     assert get_pos_args(foo) == ["bar", "baz"]
+
+
+def test_unwrap_decorators_no_decorators():
+    def foo():
+        pass
+
+    assert unwrap_decorators(foo) is foo
+
+
+def test_unwrap_decorators_one_decorator():
+    def foo():
+        pass
+
+    @functools.wraps(foo)
+    def bar():
+        return foo()
+
+    if sys.version_info < (3,):
+        bar.__wrapped__ = foo
+
+    assert unwrap_decorators(bar) is foo
Repository: scoutapp/scout_apm_python
Pull request #476 (2020-02-14 16:09:19)

Limit traceback size
Copied from the Ruby agent: https://github.com/scoutapp/scout_apm_ruby/blob/84575a57f2fb64bced6f2394632212f2bfb0268f/lib/scout_apm/layer.rb#L116

Not strictly related to implementing the issue, but spotted whilst investigating it as an idea to reduce memory usage and overhead.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b06f15..38e604c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@
   [PR #460](https://github.com/scoutapp/scout_apm_python/pull/460)).
 - Add Hug integration
   ([PR #460](https://github.com/scoutapp/scout_apm_python/pull/460)).
+- Limit size of recorded tracebacks to reduce memory usage
+  ([PR #467](https://github.com/scoutapp/scout_apm_python/pull/467)).
 
 ### Fixed
 
diff --git a/src/scout_apm/core/backtrace.py b/src/scout_apm/core/backtrace.py
index 4293850..dee91d2 100644
--- a/src/scout_apm/core/backtrace.py
+++ b/src/scout_apm/core/backtrace.py
@@ -4,12 +4,20 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 import sys
 import traceback
 
+# Maximum non-Scout frames to target retrieving
+LIMIT = 50
+# How many upper frames from inside Scout to ignore
+IGNORED = 1
+
+
 if sys.version_info >= (3, 5):
 
     def capture():
         return [
             {"file": frame.filename, "line": frame.lineno, "function": frame.name}
-            for frame in reversed(traceback.extract_stack()[:-1])
+            for frame in reversed(
+                traceback.extract_stack(limit=LIMIT + IGNORED)[:-IGNORED]
+            )
         ]
 
 
@@ -18,5 +26,7 @@ else:
     def capture():
         return [
             {"file": frame[0], "line": frame[1], "function": frame[3]}
-            for frame in reversed(traceback.extract_stack()[:-1])
+            for frame in reversed(
+                traceback.extract_stack(limit=LIMIT + IGNORED)[:-IGNORED]
+            )
         ]


Test patch:
diff --git a/tests/unit/core/test_backtrace.py b/tests/unit/core/test_backtrace.py
index 2c97066..6d3dfd7 100644
--- a/tests/unit/core/test_backtrace.py
+++ b/tests/unit/core/test_backtrace.py
@@ -26,3 +26,14 @@ def format_py_filename(filename):
         # Python 2 will include .pyc filename if it's used, so strip that
         return filename[:-1]
     return filename
+
+
+def test_capture_limit():
+    def capture_recursive_bottom(limit):
+        if limit <= 1:
+            return backtrace.capture()
+        else:
+            return capture_recursive_bottom(limit - 1)
+
+    stack = capture_recursive_bottom(backtrace.LIMIT * 2)
+    assert len(stack) == backtrace.LIMIT
Repository: scoutapp/scout_apm_python
Pull request #478 (2020-02-17 13:54:41)

Bottle copy all config keys not just known keys
This brings us in line with the other integrations' "configuration copying" implementations and means that if we drop a key from `known_keys()` it will still apply.


Patch:
diff --git a/src/scout_apm/bottle.py b/src/scout_apm/bottle.py
index 44e73b8..e81a18f 100644
--- a/src/scout_apm/bottle.py
+++ b/src/scout_apm/bottle.py
@@ -22,10 +22,12 @@ class ScoutPlugin(object):
 
     def set_config_from_bottle(self, app):
         bottle_configs = {}
-        for key in scout_config.known_keys():
-            value = app.config.get("scout.{}".format(key))
-            if value is not None and value != "":
-                bottle_configs[key] = value
+        prefix = "scout."
+        prefix_len = len(prefix)
+        for key, value in app.config.items():
+            if key.startswith(prefix) and len(key) > prefix_len:
+                scout_key = key[prefix_len:]
+                bottle_configs[scout_key] = value
         scout_config.set(**bottle_configs)
 
     def setup(self, app):


Test patch:
diff --git a/tests/integration/test_bottle.py b/tests/integration/test_bottle.py
index 5189f7c..f87583e 100644
--- a/tests/integration/test_bottle.py
+++ b/tests/integration/test_bottle.py
@@ -10,6 +10,7 @@ from webtest import TestApp
 from scout_apm.api import Config
 from scout_apm.bottle import ScoutPlugin
 from scout_apm.compat import datetime_to_timestamp, kwargs_only
+from scout_apm.core.config import scout_config
 from tests.integration.util import (
     parametrize_filtered_params,
     parametrize_queue_time_header_name,
@@ -67,6 +68,20 @@ def app_with_scout(config=None, catchall=False):
         Config.reset_all()
 
 
+def test_config_copied():
+    with app_with_scout(config={"scout.foo": "bar"}):
+        value = scout_config.value("foo")
+
+    assert value == "bar"
+
+
+def test_empty_config_not_copied():
+    with app_with_scout(config={"scout.": "foo"}):
+        value = scout_config.value("")
+
+    assert value is None
+
+
 def test_home(tracked_requests):
     with app_with_scout() as app:
         response = TestApp(app).get("/")
Repository: scoutapp/scout_apm_python
Pull request #563 (2020-09-01 14:43:00)

Fixed validation check on length of key


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a63212..e2f8453 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## Pending
+
+### Fixed
+
+- Fixed validation check on length of key.
+  ([Issue #563](https://github.com/scoutapp/scout_apm_python/issues/563))
+
 ## [2.15.2] 2020-07-24
 
 ### Fixed
@@ -7,7 +14,7 @@
 - On Django, only track the current user ID if `request.user` was accessed by
   application code. This stops a `Vary: Cookie` header being injected due to
   session data access.
-  ([Issue #544](https://github.com/scoutapp/scout_apm_python/issues/544)).
+  ([Issue #544](https://github.com/scoutapp/scout_apm_python/issues/544))
 
 ## [2.15.1] 2020-07-23
 
diff --git a/src/scout_apm/core/commands.py b/src/scout_apm/core/commands.py
index 7e86f42..bc1eb03 100644
--- a/src/scout_apm/core/commands.py
+++ b/src/scout_apm/core/commands.py
@@ -8,7 +8,7 @@ from scout_apm.compat import iteritems
 
 logger = logging.getLogger(__name__)
 
-key_regex = re.compile(r"^[a-zA-Z0-9]{16}$")
+key_regex = re.compile(r"^[a-zA-Z0-9]{20}$")
 
 
 class Register(object):


Test patch:
diff --git a/tests/unit/core/test_commands.py b/tests/unit/core/test_commands.py
index ad95c8c..4e38b54 100644
--- a/tests/unit/core/test_commands.py
+++ b/tests/unit/core/test_commands.py
@@ -26,13 +26,13 @@ END_TIME_STR = "2018-12-01T17:04:34.641403Z"
 
 def test_register_message_good_key(caplog):
     command = commands.Register(
-        app="test_app", key="abcdefghijabcdef", hostname="test_host"
+        app="test_app", key="abcdefghijabcdefghij", hostname="test_host"
     )
 
     assert command.message() == {
         "Register": {
             "app": "test_app",
-            "key": "abcdefghijabcdef",
+            "key": "abcdefghijabcdefghij",
             "host": "test_host",
             "language": "python",
             "api_version": "1.0",
Repository: scoutapp/scout_apm_python
Pull request #598 (2021-01-20 17:59:02)

Support hexidecimal amazon time strings.
Occassionally Amazon will send the time string in hexidecimal format.
It's not parseable in the format, but they do indicate it will be 8
characters long. It should be reasonable that decimal format time strings
will be longer than that.


Patch:
diff --git a/src/scout_apm/core/web_requests.py b/src/scout_apm/core/web_requests.py
index f4173dd..190fca5 100644
--- a/src/scout_apm/core/web_requests.py
+++ b/src/scout_apm/core/web_requests.py
@@ -137,8 +137,10 @@ def track_amazon_request_queue_time(header_value, tracked_request):
     if not first_char.isdigit():
         return False
 
+    # If the timestamp is 8 characters long, it's in hexadecimal format.
+    base = 16 if len(timestamp_str) == 8 else 10
     try:
-        start_timestamp_ns = int(timestamp_str) * 1000000000.0
+        start_timestamp_ns = int(timestamp_str, base) * 1000000000.0
     except ValueError:
         return False
 


Test patch:
diff --git a/tests/unit/core/test_web_requests.py b/tests/unit/core/test_web_requests.py
index c756559..23375b7 100644
--- a/tests/unit/core/test_web_requests.py
+++ b/tests/unit/core/test_web_requests.py
@@ -146,6 +146,27 @@ def test_track_amazon_request_queue_time_valid(header_value, tracked_request):
     queue_time_ns = tracked_request.tags["scout.queue_time_ns"]
     assert isinstance(queue_time_ns, int) and queue_time_ns > 0
 
+@pytest.mark.parametrize(
+    "header_value",
+    [
+        "Root=1-{start_time}-12456789abcdef012345678",
+        "Root=1-{start_time}-12456789abcdef012345678;CalledFrom=app",
+        "Self=1-{start_time}-12456789abcdef012345678",
+        "Self=1-{start_time}-12456789abcdef012345678;Root=1-123-abcdef012345678912345678",  # noqa: E501
+        "Self=1-{start_time}-12456789abcdef012345678;Root=1-123-abcdef012345678912345678;CalledFrom=app",  # noqa: E501
+    ],
+)
+def test_track_amazon_request_queue_time_hexidecimal_valid(header_value, tracked_request):
+    start_time = int(datetime_to_timestamp(dt.datetime.utcnow()), 16) - 2
+
+    result = track_amazon_request_queue_time(
+        "Root=1-{start_time}-12456789abcdef012345678".format(start_time=start_time), tracked_request
+    )
+
+    assert result is True
+    queue_time_ns = tracked_request.tags["scout.queue_time_ns"]
+    assert isinstance(queue_time_ns, int) and queue_time_ns > 0
+
 
 @pytest.mark.parametrize(
     "header_value",