Repository: getappmap/appmap-python (language: python)

Issue:
Non-JSON serializable types throw an error when recorded with AppMap
Non-JSON serializable types can cause the application to panic.

```txt
FAILED tests/test.pt - TypeError: Object of type int64 is not JSON serializable
1 failed, 29 passed, 39 warnings in 10.53s
```

```txt
_appmap/generation.py:114: in default
    return json.JSONEncoder.default(self, o)
```

via https://appmap-group.slack.com/archives/C03GM7D2QRH/p1708366829827279

https://github.com/getappmap/appmap-python/issues/312


Discussion / hints:
Rather than adding a special case for `int64` to `AppMapEncoder`, we need to figure out a better way of handling unknown types.

Maybe something like
```python
try:
    return json.JSONEncoder.default(self, o)
except TypeError:
    return repr(o) # or maybe str(o)
```

It would probably be good if the tests for this fix include some sort of simple app that uses `numpy`.


Fix patch:
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 2210a524..f70988fa 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -6,4 +6,5 @@ pytest-django<4.8
 fastapi
 httpx
 sqlalchemy
-debugpy
\ No newline at end of file
+debugpy
+numpy
\ No newline at end of file
diff --git a/tox.ini b/tox.ini
index ea4ec756..faeb02af 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,6 +18,8 @@ allowlist_externals =
 deps=
     poetry
     web: {[web-deps]deps}
+    py38: numpy==1.24.4
+    py3{9,10,11,12}: numpy >=1.26
     flask2: Flask >= 2.0, <3.0
     django3: Django >=3.2, <4.0
     sqlalchemy1: sqlalchemy >=1.4.11, <2.0


Test patch:
diff --git a/_appmap/test/data/pytest/expected/pytest.appmap.json b/_appmap/test/data/pytest/expected/pytest.appmap.json
index 5f047ad0..8723789a 100644
--- a/_appmap/test/data/pytest/expected/pytest.appmap.json
+++ b/_appmap/test/data/pytest/expected/pytest.appmap.json
@@ -8,95 +8,133 @@
       "name": "appmap",
       "url": "https://github.com/applandinc/appmap-python"
     },
+    "source_location": "test_simple.py:5",
+    "name": "hello world",
+    "feature": "Hello world",
     "app": "Simple",
     "recorder": {
       "name": "pytest",
       "type": "tests"
     },
-    "source_location": "test_simple.py:5",
-    "name": "hello world",
-    "feature": "Hello world",
     "test_status": "succeeded"
   },
   "events": [
     {
-      "defined_class": "simple.Simple",
-      "method_id": "hello_world",
-      "path": "simple.py",
-      "lineno": 8,
       "static": false,
       "receiver": {
-        "class": "simple.Simple",
         "kind": "req",
+        "value": "<simple.Simple object at 0xabcdef>",
         "name": "self",
-        "value": "<simple.Simple object at 0xabcdef>"
+        "class": "simple.Simple"
       },
       "parameters": [],
       "id": 1,
       "event": "call",
-      "thread_id": 1
+      "thread_id": 1,
+      "defined_class": "simple.Simple",
+      "method_id": "hello_world",
+      "path": "simple.py",
+      "lineno": 16
     },
     {
+      "static": false,
+      "receiver": {
+        "kind": "req",
+        "value": "<simple.Simple object at 0xabcdef>",
+        "name": "self",
+        "class": "simple.Simple"
+      },
+      "parameters": [],
+      "id": 2,
+      "event": "call",
+      "thread_id": 1,
       "defined_class": "simple.Simple",
-      "method_id": "hello",
+      "method_id": "get_non_json_serializable",
       "path": "simple.py",
-      "lineno": 2,
+      "lineno": 13
+    },
+    {
       "static": false,
       "receiver": {
-        "class": "simple.Simple",
         "kind": "req",
+        "value": "<simple.Simple object at 0xabcdef>",
         "name": "self",
-        "value": "<simple.Simple object at 0xabcdef>"
+        "class": "simple.Simple"
       },
       "parameters": [],
-      "id": 2,
+      "id": 3,
       "event": "call",
-      "thread_id": 1
+      "thread_id": 1,
+      "defined_class": "simple.Simple",
+      "method_id": "hello",
+      "path": "simple.py",
+      "lineno": 7
     },
     {
       "return_value": {
-        "class": "builtins.str",
-        "value": "'Hello'"
+        "value": "'Hello'",
+        "class": "builtins.str"
       },
-      "parent_id": 2,
-      "id": 3,
+      "parent_id": 3,
+      "id": 4,
       "event": "return",
       "thread_id": 1
     },
     {
-      "defined_class": "simple.Simple",
-      "method_id": "world",
-      "path": "simple.py",
-      "lineno": 5,
       "static": false,
       "receiver": {
-        "class": "simple.Simple",
         "kind": "req",
+        "value": "<simple.Simple object at 0xabcdef>",
         "name": "self",
-        "value": "<simple.Simple object at 0xabcdef>"
+        "class": "simple.Simple"
       },
       "parameters": [],
-      "id": 4,
+      "id": 5,
       "event": "call",
+      "thread_id": 1,
+      "defined_class": "simple.Simple",
+      "method_id": "world",
+      "path": "simple.py",
+      "lineno": 10
+    },
+    {
+      "return_value": {
+        "value": "'world!'",
+        "class": "builtins.str"
+      },
+      "parent_id": 5,
+      "id": 6,
+      "event": "return",
       "thread_id": 1
     },
     {
       "return_value": {
-        "class": "builtins.str",
-        "value": "'world!'"
+        "value": "{0: 'Hello', 1: 'world!'}",
+        "class": "builtins.dict",
+        "properties": [
+          {
+            "name": "0",
+            "class": "builtins.str"
+          },
+          {
+            "name": "1",
+            "class": "builtins.str"
+          }
+        ],
+        "size": 2
       },
-      "parent_id": 4,
-      "id": 5,
+      "parent_id": 2,
+      "id": 7,
       "event": "return",
       "thread_id": 1
     },
     {
       "return_value": {
-        "class": "builtins.str",
-        "value": "'Hello world!'"
+        "value": "'Hello world!'",
+        "class": "builtins.str"
       },
       "parent_id": 1,
-      "id": 6,
+      "id": 8,
       "event": "return",
       "thread_id": 1
     }
@@ -110,22 +148,28 @@
           "name": "Simple",
           "type": "class",
           "children": [
+            {
+              "name": "get_non_json_serializable",
+              "type": "function",
+              "location": "simple.py:13",
+              "static": false
+            },
             {
               "name": "hello",
               "type": "function",
-              "location": "simple.py:2",
+              "location": "simple.py:7",
               "static": false
             },
             {
               "name": "hello_world",
               "type": "function",
-              "location": "simple.py:8",
+              "location": "simple.py:16",
               "static": false
             },
             {
               "name": "world",
               "type": "function",
-              "location": "simple.py:5",
+              "location": "simple.py:10",
               "static": false
             }
           ]
diff --git a/_appmap/test/data/pytest/simple.py b/_appmap/test/data/pytest/simple.py
index eb824400..af0338eb 100644
--- a/_appmap/test/data/pytest/simple.py
+++ b/_appmap/test/data/pytest/simple.py
@@ -1,3 +1,8 @@
+import numpy
+
+zero = numpy.int64(0)
+one = numpy.int64(1)
+
 class Simple:
     def hello(self):
         return "Hello"
@@ -5,5 +10,9 @@ def hello(self):
     def world(self):
         return "world!"
 
+    def get_non_json_serializable(self):
+        return { zero: self.hello(), one: self.world() }
+
     def hello_world(self):
-        return "%s %s" % (self.hello(), self.world())
+        result = self.get_non_json_serializable()
+        return "%s %s" % (result[zero], result[one])
diff --git a/_appmap/test/test_generation.py b/_appmap/test/test_generation.py
index e7891d60..ee1762fa 100644
--- a/_appmap/test/test_generation.py
+++ b/_appmap/test/test_generation.py
@@ -1,5 +1,10 @@
+import json
 import pytest
 
+import numpy as np
+
+from _appmap.generation import AppMapEncoder
+
 
 @pytest.mark.appmap_enabled
 @pytest.mark.usefixtures("with_data_dir")
@@ -48,3 +53,18 @@ def check_comment(self, to_dict):
             return ret
 
         verify_example_appmap(check_comment, "instance_method")
+
+class TestAppMapEncoder:
+    def test_np_int64_type(self):
+        data = {
+            "value": np.int64(42),
+        }
+        json_str = json.dumps(data, cls=AppMapEncoder)
+        assert '{"value": "42"}' == json_str
+
+    def test_np_array_type(self):
+        data = {
+            "value": np.array([0, 1, 2, 3])
+        }
+        json_str = json.dumps(data, cls=AppMapEncoder)
+        assert '{"value": "[0 1 2 3]"}' == json_str