Skip to content

Commit ab197e0

Browse files
committed
feat(pipeline): IE errors translation
1 parent 28e6f39 commit ab197e0

File tree

4 files changed

+199
-0
lines changed

4 files changed

+199
-0
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from __future__ import absolute_import, print_function
2+
3+
import six
4+
import os
5+
import io
6+
import re
7+
8+
LOCALES_DIR = os.path.join(os.path.dirname(__file__), '../../data/error-locale')
9+
TARGET_LOCALE = 'en-US'
10+
11+
translation_lookup_table = set()
12+
target_locale_lookup_table = {}
13+
14+
for locale in os.listdir(LOCALES_DIR):
15+
fn = os.path.join(LOCALES_DIR, locale)
16+
if not os.path.isfile(fn):
17+
continue
18+
19+
with io.open(fn, encoding='utf-8') as f:
20+
for line in f:
21+
key, translation = line.split(',', 1)
22+
translation = translation.strip()
23+
24+
if TARGET_LOCALE in locale:
25+
target_locale_lookup_table[key] = translation
26+
else:
27+
translation_regexp = re.escape(translation)
28+
translation_regexp = translation_regexp.replace(
29+
'\%s', r'(?P<format_string_data>[a-zA-Z0-9-_\$]+)')
30+
# Some errors are substrings of more detailed ones, so we need exact match
31+
translation_regexp = re.compile('^' + translation_regexp + '$')
32+
translation_lookup_table.add((translation_regexp, key))
33+
34+
35+
def find_translation(message):
36+
for translation in translation_lookup_table:
37+
translation_regexp, key = translation
38+
match = translation_regexp.search(message)
39+
40+
if match is not None:
41+
format_string_data = match.groupdict().get('format_string_data')
42+
43+
if format_string_data is None:
44+
return [key, None]
45+
else:
46+
return [key, format_string_data]
47+
48+
return [None, None]
49+
50+
51+
def format_message(message, data):
52+
return message.replace('%s', data)
53+
54+
message_type_regexp = re.compile('^(?P<type>[a-zA-Z]*Error): (?P<message>.*)')
55+
56+
57+
def translate_message(original_message):
58+
if not isinstance(original_message, six.string_types):
59+
return original_message
60+
61+
type = None
62+
message = original_message.strip()
63+
64+
# Handle both cases. Just a message and message preceeded with error type
65+
# eg. `ReferenceError: foo`, `TypeError: bar`
66+
match = message_type_regexp.search(message)
67+
68+
if match is not None:
69+
type = match.groupdict().get('type')
70+
message = match.groupdict().get('message')
71+
72+
translation, format_string_data = find_translation(message)
73+
74+
if translation is None:
75+
return original_message
76+
else:
77+
translated_message = target_locale_lookup_table.get(translation, original_message)
78+
79+
if type is not None:
80+
translated_message = type + ': ' + translated_message
81+
82+
if format_string_data is None:
83+
return translated_message
84+
else:
85+
return format_message(translated_message, format_string_data)
86+
87+
88+
def translate_exception(data):
89+
if 'sentry.interfaces.Message' in data:
90+
data['sentry.interfaces.Message']['message'] = translate_message(
91+
data['sentry.interfaces.Message']['message'])
92+
93+
if 'sentry.interfaces.Exception' in data:
94+
for entry in data['sentry.interfaces.Exception']['values']:
95+
if 'value' in entry:
96+
entry['value'] = translate_message(entry['value'])
97+
98+
return data

src/sentry/lang/javascript/plugin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77

88
from .processor import JavaScriptStacktraceProcessor
99
from .errormapping import rewrite_exception
10+
from .errorlocale import translate_exception
1011

1112

1213
def preprocess_event(data):
1314
rewrite_exception(data)
15+
translate_exception(data)
1416
fix_culprit(data)
1517
inject_device_data(data)
1618
generate_modules(data)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import absolute_import, unicode_literals
2+
3+
from sentry.testutils import TestCase
4+
from sentry.lang.javascript.errorlocale import translate_message
5+
6+
7+
class ErrorLocaleTest(TestCase):
8+
def test_basic_translation(self):
9+
actual = 'Type mismatch'
10+
expected = translate_message('Typenkonflikt')
11+
assert actual == expected
12+
13+
def test_unicode_translation(self):
14+
expected = 'Division by zero'
15+
actual = translate_message(u'Divisi\xf3n por cero')
16+
assert actual == expected
17+
18+
def test_same_translation(self):
19+
expected = 'Out of memory'
20+
actual = translate_message('Out of memory')
21+
assert actual == expected
22+
23+
def test_unknown_translation(self):
24+
expected = 'Some unknown message'
25+
actual = translate_message('Some unknown message')
26+
assert actual == expected
27+
28+
def test_translation_with_type(self):
29+
expected = 'RangeError: Subscript out of range'
30+
actual = translate_message('RangeError: Indeks poza zakresem')
31+
assert actual == expected
32+
33+
def test_translation_with_type_and_colon(self):
34+
expected = 'RangeError: Cannot define property: object is not extensible'
35+
actual = translate_message(
36+
u'RangeError: Nie mo\u017cna zdefiniowa\u0107 w\u0142a\u015bciwo\u015bci: obiekt nie jest rozszerzalny')
37+
assert actual == expected
38+
39+
def test_interpolated_translation(self):
40+
expected = 'Type \'foo\' not found'
41+
actual = translate_message(u'Nie odnaleziono typu \u201efoo\u201d')
42+
assert actual == expected
43+
44+
def test_interpolated_translation_with_colon(self):
45+
expected = '\'this\' is not of expected type: foo'
46+
actual = translate_message(
47+
u'Typ obiektu \u201ethis\u201d jest inny ni\u017c oczekiwany: foo')
48+
assert actual == expected
49+
50+
def test_interpolated_translation_with_colon_in_front(self):
51+
expected = 'foo: an unexpected failure occurred while trying to obtain metadata information'
52+
actual = translate_message(
53+
u'foo: wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas pr\xf3by uzyskania informacji o metadanych')
54+
assert actual == expected
55+
56+
def test_interpolated_translation_with_type(self):
57+
expected = 'TypeError: Type \'foo\' not found'
58+
actual = translate_message(u'TypeError: Nie odnaleziono typu \u201efoo\u201d')
59+
assert actual == expected
60+
61+
def test_interpolated_translation_with_type_and_colon(self):
62+
expected = 'ReferenceError: Cannot modify property \'foo\': \'length\' is not writable'
63+
actual = translate_message(
64+
u'ReferenceError: Nie mo\u017cna zmodyfikowa\u0107 w\u0142a\u015bciwo\u015bci \u201efoo\u201d: warto\u015b\u0107 \u201elength\u201d jest niezapisywalna')
65+
assert actual == expected

tests/sentry/lang/javascript/test_plugin.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,40 @@ def test_inlined_sources(self, mock_discover_sourcemap, mock_fetch_file):
239239
assert not frame.post_context
240240
assert frame.data['sourcemap'] == 'http://example.com/test.min.js'
241241

242+
@responses.activate
243+
def test_error_message_translations(self):
244+
data = {
245+
'message': 'hello',
246+
'platform': 'javascript',
247+
'sentry.interfaces.Message': {
248+
'message': u'ReferenceError: Impossible de d\xe9finir une propri\xe9t\xe9 \xab foo \xbb : objet non extensible'
249+
},
250+
'sentry.interfaces.Exception': {
251+
'values': [
252+
{
253+
'type': 'Error',
254+
'value': u'P\u0159\xedli\u0161 mnoho soubor\u016f'
255+
},
256+
{
257+
'type': 'Error',
258+
'value': u'foo: wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas pr\xf3by uzyskania informacji o metadanych'
259+
}
260+
],
261+
}
262+
}
263+
264+
resp = self._postWithHeader(data)
265+
assert resp.status_code, 200
266+
267+
event = Event.objects.get()
268+
269+
message = event.interfaces['sentry.interfaces.Message']
270+
assert message.message == 'ReferenceError: Cannot define property \'foo\': object is not extensible'
271+
272+
exception = event.interfaces['sentry.interfaces.Exception']
273+
assert exception.values[0].value == 'Too many files'
274+
assert exception.values[1].value == 'foo: an unexpected failure occurred while trying to obtain metadata information'
275+
242276
@responses.activate
243277
def test_sourcemap_source_expansion(self):
244278
responses.add(

0 commit comments

Comments
 (0)