Repository: rollbar/pyrollbar

Issue:
Ability to customize how objects are repr()'d
pyrollbar uses reprlib to get representations of objects gathered as local variables. This means that to customize it, the only current option is to define/override `__repr__`. That isn't a good option in some cases, including:
- objects coming from external libraries
- objects where something else depends on the existing `__repr__` implementation

Creating this issue to discuss the design for how to make this customizable.

Discussion:
@coryvirok: Thanks for starting the convo, Brian. 

I would probably expose a new configuration option that would allow the user to provide a function that would be used instead of `__repr__()` when serializing the local variable data.

e.g. 

``` py
# Ordered list of serializers. The first regex pattern that 
# matches the object being serialized will be used. 
# If none of the user-supplied patterns match, the default 
# reprlib.repr() serializer will be used.
serializers = [
  ('class_name_regex_pattern', my_custom_serializer_fn)
  ('*', reprlib.repr)
]

```
@sibson: +1 as is.  It might be nice to support a `__rollbar_repr__()` convention of some kind for internal code.
@jfarrimo: We need to make sure the solution to this addresses the problem that this pull request attempted to address. https://github.com/rollbar/pyrollbar/pull/91
@rivkahstandig3636: Hi there, I’m closing out all issues opened before 2018 that haven’t had any activity on them since the start of this year. If this is still an issue for you, please comment here and we can reopen this. Thanks!@dariatsenter: Would be nice to have this option!@danstewart: Another option here could be to use [devtools](https://pypi.org/project/devtools/) for formatting.


Solution patch:
diff --git a/rollbar/lib/transform.py b/rollbar/lib/transform.py
index 70b957b..e372066 100644
--- a/rollbar/lib/transform.py
+++ b/rollbar/lib/transform.py
@@ -1,3 +1,6 @@
+from typing import Optional
+
+
 class Transform(object):
     depth_first = True
     priority = 100
@@ -40,3 +43,12 @@ class Transform(object):
 
     def transform_custom(self, o, key=None):
         return self.default(o, key=key)
+
+    @staticmethod
+    def rollbar_repr(obj: object) -> Optional[str]:
+        r = None
+        if hasattr(obj, '__rollbar_repr__'):
+            r = obj.__rollbar_repr__()
+            if not isinstance(r, str):
+                raise TypeError(f'__rollbar_repr__ returned non-string (type {type(r)})')
+        return r
diff --git a/rollbar/lib/transforms/serializable.py b/rollbar/lib/transforms/serializable.py
index f426f2d..832d0ab 100644
--- a/rollbar/lib/transforms/serializable.py
+++ b/rollbar/lib/transforms/serializable.py
@@ -73,6 +73,11 @@ class SerializableTransform(Transform):
         # Best to be very careful when we call user code in the middle of
         # preparing a stack trace. So we put a try/except around it all.
         try:
+            # If the object has a __rollbar_repr__() method, use it.
+            custom = Transform.rollbar_repr(o)
+            if custom is not None:
+                return custom
+
             if any(filter(lambda x: isinstance(o, x), self.safelist)):
                 try:
                     return repr(o)
diff --git a/rollbar/lib/transforms/shortener.py b/rollbar/lib/transforms/shortener.py
index 1717673..fd5a35b 100644
--- a/rollbar/lib/transforms/shortener.py
+++ b/rollbar/lib/transforms/shortener.py
@@ -105,7 +105,6 @@ def shorten_tuple(obj: tuple, max_len: int) -> tuple:
     return obj[:max_len] + ('...',)
 
 
-
 class ShortenerTransform(Transform):
     depth_first = False
     priority = 10
@@ -163,6 +162,11 @@ class ShortenerTransform(Transform):
         if obj is None:
             return None
 
+        # If the object has a __rollbar_repr__() method, use it.
+        custom = Transform.rollbar_repr(obj)
+        if custom is not None:
+            return custom
+
         if self.safe_repr:
             obj = str(obj)
 


Test patch:
diff --git a/rollbar/test/test_serializable_transform.py b/rollbar/test/test_serializable_transform.py
index 6978d7c..1338eb7 100644
--- a/rollbar/test/test_serializable_transform.py
+++ b/rollbar/test/test_serializable_transform.py
@@ -189,6 +189,16 @@ class SerializableTransformTest(BaseTest):
         del expected[invalid]
         self._assertSerialized(start, expected)
 
+    def test_encode_with_rollbar_repr(self):
+        class CustomRepr(object):
+            def __rollbar_repr__(self):
+                return 'hello'
+
+        start = {'hello': 'world', 'custom': CustomRepr()}
+        expected = copy.deepcopy(start)
+        expected['custom'] = 'hello'
+        self._assertSerialized(start, expected)
+
     def test_encode_with_custom_repr_no_safelist(self):
         class CustomRepr(object):
             def __repr__(self):
diff --git a/rollbar/test/test_shortener_transform.py b/rollbar/test/test_shortener_transform.py
index c1f5fe7..eeaf38e 100644
--- a/rollbar/test/test_shortener_transform.py
+++ b/rollbar/test/test_shortener_transform.py
@@ -130,6 +130,19 @@ class ShortenerTransformTest(BaseTest):
         self.assertEqual(type(result), dict)
         self.assertEqual(len(result['request']['POST']), 10)
 
+    def test_shorten_custom_rollbar_repr(self):
+        class CustomObj:
+            value = 'value'
+            def __rollbar_repr__(self):
+                return f'<custom: {self.value}>'
+
+        obj = CustomObj()
+
+        original = obj
+        shortened = '<custom: value>'
+        self.assertEqual(shortened, self.shortener.default(original, ('shorten',)))
+        self.assertEqual(original, self.shortener.default(original, ('nope',)))
+
     def test_shorten_frame(self):
         data = {
             'body': {