Repository: DataDog/dd-trace-py

Issue:
Using botocore.Stubber without ResponseMetadata fails
### Which version of dd-trace-py are you using?

0.45.0, but the code is the same on 0.47.0

### Which version of the libraries are you using?

botocore==1.16.15

### How can we reproduce your problem?

Write a test using [Stubber](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/stubber.html#botocore-stub):

```python
import datetime

import ddtrace
ddtrace.patch_all()

import botocore.session
from botocore.stub import Stubber

s3 = botocore.session.get_session().create_client('s3', aws_access_key_id="foo", aws_secret_access_key="bar")

response = {
    "Owner": {
        "ID": "foo",
        "DisplayName": "bar"
    },
    "Buckets": [{
        "CreationDate": datetime.datetime(2016, 1, 20, 22, 9),
        "Name": "baz"
    }]
}

with Stubber(s3) as stubber:
    stubber.add_response('list_buckets', response, {})
    service_response = s3.list_buckets()

assert service_response == response
```

### What is the result that you get?

```
Traceback (most recent call last):
  File "main.py", line 24, in <module>
    service_response = s3.list_buckets()
  File "/opt/virtualenvs/python3/lib/python3.8/site-packages/botocore/client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/opt/virtualenvs/python3/lib/python3.8/site-packages/ddtrace/contrib/botocore/patch.py", line 166, in patched_api_call
    response_meta = result['ResponseMetadata']
KeyError: 'ResponseMetadata'
```

### What is the result that you expected?

No exceptions.


Discussion:

Resolving patch:
diff --git a/ddtrace/contrib/botocore/patch.py b/ddtrace/contrib/botocore/patch.py
--- a/ddtrace/contrib/botocore/patch.py
+++ b/ddtrace/contrib/botocore/patch.py
@@ -157,16 +157,16 @@ def patched_api_call(original_func, instance, args, kwargs):
 
         result = original_func(*args, **kwargs)
 
-        response_meta = result["ResponseMetadata"]
+        response_meta = result.get("ResponseMetadata")
+        if response_meta:
+            if "HTTPStatusCode" in response_meta:
+                span.set_tag(http.STATUS_CODE, response_meta["HTTPStatusCode"])
 
-        if "HTTPStatusCode" in response_meta:
-            span.set_tag(http.STATUS_CODE, response_meta["HTTPStatusCode"])
+            if "RetryAttempts" in response_meta:
+                span.set_tag("retry_attempts", response_meta["RetryAttempts"])
 
-        if "RetryAttempts" in response_meta:
-            span.set_tag("retry_attempts", response_meta["RetryAttempts"])
-
-        if "RequestId" in response_meta:
-            span.set_tag("aws.requestid", response_meta["RequestId"])
+            if "RequestId" in response_meta:
+                span.set_tag("aws.requestid", response_meta["RequestId"])
 
         # set analytics sample rate
         span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.botocore.get_analytics_sample_rate())


Test patch:
diff --git a/tests/contrib/botocore/test.py b/tests/contrib/botocore/test.py
--- a/tests/contrib/botocore/test.py
+++ b/tests/contrib/botocore/test.py
@@ -1,7 +1,9 @@
 # 3p
 import base64
+import datetime
 import io
 import json
+import unittest
 import zipfile
 
 import botocore.session
@@ -26,6 +28,10 @@
 from ... import assert_span_http_status_code
 
 
+# Parse botocore.__version_ from "1.9.0" to (1, 9, 0)
+BOTOCORE_VERSION = tuple(int(x) for x in botocore.__version__.split("."))
+
+
 def get_zip_lambda():
     code = """
 def lambda_handler(event, context):
@@ -686,3 +692,19 @@ def test_traced_client_ot(self):
         self.assertEqual(dd_span.service, "test-botocore-tracing.ec2")
         self.assertEqual(dd_span.resource, "ec2.describeinstances")
         self.assertEqual(dd_span.name, "ec2.command")
+
+    @unittest.skipIf(BOTOCORE_VERSION < (1, 9, 0), "Skipping for older versions of botocore without Stubber")
+    def test_stubber_no_response_metadata(self):
+        """When no ResponseMetadata key is provided in the response"""
+        from botocore.stub import Stubber
+
+        response = {
+            "Owner": {"ID": "foo", "DisplayName": "bar"},
+            "Buckets": [{"CreationDate": datetime.datetime(2016, 1, 20, 22, 9), "Name": "baz"}],
+        }
+
+        s3 = self.session.create_client("s3", aws_access_key_id="foo", aws_secret_access_key="bar")
+        with Stubber(s3) as stubber:
+            stubber.add_response("list_buckets", response, {})
+            service_response = s3.list_buckets()
+            assert service_response == response