Repository: getsentry/sentry

Issue:
Source map lookups wrong when location is first position in state array
Failing test here: https://github.com/getsentry/sentry/commit/289e4e664769173b3b9ef7eb34c3e2576a2a543f

Source of bug in [sentry.lang.javascript.sourcemaps#L158](https://github.com/getsentry/sentry/blob/master/src/sentry/lang/javascript/sourcemaps.py#L158):

``` python
def find_source(indexed_sourcemap, lineno, colno):
    # error says "line no 1, column no 56"
    assert lineno > 0, 'line numbers are 1-indexed'
    return indexed_sourcemap.states[bisect.bisect_left(indexed_sourcemap.keys, (lineno - 1, colno)) - 1]
```

Fails because `bisect_left` will return the index to the left of a match if there is an exact match. In this test case, the match is the first state `(0, 0)`, after which we subtract `1` and access the array at location `-1` ... which is the last element in the array.

Me debugging the test case in pdb:

```
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> sentry/src/sentry/lang/javascript/sourcemaps.py(157)find_source()
-> assert lineno > 0, 'line numbers are 1-indexed'
(Pdb) lineno
1
(Pdb) colno
0
(Pdb) bisect.bisect_left(indexed_sourcemap.keys, (lineno - 1, colno)) - 1
-1
(Pdb) indexed_sourcemap.keys
[(0, 0), (0, 8), (0, 13), (0, 15), ..., (0, 168), (0, 174), (0, 191)]
(Pdb) indexed_sourcemap.states[-1]
SourceMap(dst_line=0, dst_col=191, src='foo/file2.js', src_line=9, src_col=25, name='e')
```

---

It appears that @mattrobenolt's python-sourcemap [implementation](https://github.com/mattrobenolt/python-sourcemap/blob/master/sourcemap/objects.py#L70) uses `bisect_right`, but I haven't investigated to see if the bug is present there.



Discussion:

Resolving patch:
diff --git a/src/sentry/lang/javascript/sourcemaps.py b/src/sentry/lang/javascript/sourcemaps.py
--- a/src/sentry/lang/javascript/sourcemaps.py
+++ b/src/sentry/lang/javascript/sourcemaps.py
@@ -155,4 +155,5 @@ def sourcemap_to_index(sourcemap):
 def find_source(indexed_sourcemap, lineno, colno):
     # error says "line no 1, column no 56"
     assert lineno > 0, 'line numbers are 1-indexed'
-    return indexed_sourcemap.states[bisect.bisect_left(indexed_sourcemap.keys, (lineno - 1, colno)) - 1]
+
+    return indexed_sourcemap.states[bisect.bisect_right(indexed_sourcemap.keys, (lineno - 1, colno)) - 1]


Test patch:
diff --git a/tests/sentry/lang/javascript/test_sourcemaps.py b/tests/sentry/lang/javascript/test_sourcemaps.py
--- a/tests/sentry/lang/javascript/test_sourcemaps.py
+++ b/tests/sentry/lang/javascript/test_sourcemaps.py
@@ -34,9 +34,24 @@ def test_simple(self):
         indexed_sourcemap = sourcemap_to_index(sourcemap)
 
         result = find_source(indexed_sourcemap, 1, 56)
-
         assert result == SourceMap(dst_line=0, dst_col=50, src='foo/file2.js', src_line=0, src_col=9, name='multiply')
 
+        # Start of minified file (exact match first line/col tuple)
+        result = find_source(indexed_sourcemap, 1, 0)
+        assert result == SourceMap(dst_line=0, dst_col=0, src='foo/file1.js', src_line=0, src_col=0, name=None)
+
+        # Last character in mapping
+        result = find_source(indexed_sourcemap, 1, 36)
+        assert result == SourceMap(dst_line=0, dst_col=30, src='foo/file1.js', src_line=2, src_col=1, name=None)
+
+        # First character in mapping (exact match line/col tuple)
+        result = find_source(indexed_sourcemap, 1, 37)
+        assert result == SourceMap(dst_line=0, dst_col=37, src='foo/file1.js', src_line=2, src_col=8, name='a')
+
+        # End of minified file (character *beyond* last line/col tuple)
+        result = find_source(indexed_sourcemap, 1, 192)
+        assert result == SourceMap(dst_line=0, dst_col=191, src='foo/file2.js', src_line=9, src_col=25, name='e')
+
 
 class ParseSourcemapTest(TestCase):
     def test_basic(self):