From 8d1ae15994fd351faf808813293e8c8611e8a150 Mon Sep 17 00:00:00 2001 From: Ramon Niebla Date: Mon, 19 Oct 2020 19:05:30 -0700 Subject: [PATCH 01/31] quick context manager prototype --- rollbar/__init__.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index c40f4b78..a3c64455 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -702,6 +702,10 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if extra_trace_data and not extra_data: data['custom'] = extra_trace_data + global feature_flags_data + data['feature_flags'] = feature_flags_data + feature_flags_data = {} + request = _get_actual_request(request) _add_request_data(data, request) _add_person_data(data, request) @@ -787,6 +791,10 @@ def _report_message(message, level, request, extra_data, payload_data): _add_person_data(data, request) _add_lambda_context_data(data) data['server'] = _build_server_data() + + global feature_flags_data + data['feature_flags'] = feature_flags_data + feature_flags_data = {} if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1362,6 +1370,8 @@ def _serialize_payload(payload): return json.dumps(payload, default=defaultJSONEncode) +feature_flags_data = {} + def _send_payload(payload_str, access_token): try: _post_api('item/', payload_str, access_token=access_token) @@ -1606,3 +1616,32 @@ def _wsgi_extract_user_ip(environ): if real_ip: return real_ip return environ['REMOTE_ADDR'] + + +class FeatureFlags(object): + def __init__(self, flag_key): + self.flag_key = flag_key + + def __enter__(self): + try: + import ldclient + except ImportError: + log.info('Launch Darkly not available') + return + + variation = ldclient.get().variation(self.flag_key) + + global feature_flags_data + + feature_flags_data[self.flag_key] = { + 'variation': variation, + # maybe we could use some more data, leaving as placeholder + } + + return variation + + def __exit__(self, type, value, traceback): + global feature_flags_data + del feature_flags_data[self.flag_key] + + From 529f0a49db507ab79389c41ec84a59c66cddfa07 Mon Sep 17 00:00:00 2001 From: Ramon Niebla Date: Tue, 20 Oct 2020 08:39:23 -0700 Subject: [PATCH 02/31] adding some defaults --- rollbar/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index a3c64455..a2753233 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -1619,7 +1619,7 @@ def _wsgi_extract_user_ip(environ): class FeatureFlags(object): - def __init__(self, flag_key): + def __init__(self, flag_key, default): self.flag_key = flag_key def __enter__(self): @@ -1629,7 +1629,7 @@ def __enter__(self): log.info('Launch Darkly not available') return - variation = ldclient.get().variation(self.flag_key) + variation = ldclient.get().variation(self.flag_key, {}, False) global feature_flags_data From 5bac851c06ccfb5e8a1fd0a0c9a9d47d3851a905 Mon Sep 17 00:00:00 2001 From: Ramon Niebla Date: Tue, 20 Oct 2020 09:05:51 -0700 Subject: [PATCH 03/31] default param added --- rollbar/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index a2753233..844f7790 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -1619,7 +1619,7 @@ def _wsgi_extract_user_ip(environ): class FeatureFlags(object): - def __init__(self, flag_key, default): + def __init__(self, flag_key, default=False): self.flag_key = flag_key def __enter__(self): @@ -1629,7 +1629,7 @@ def __enter__(self): log.info('Launch Darkly not available') return - variation = ldclient.get().variation(self.flag_key, {}, False) + variation = ldclient.get().variation(self.flag_key, {}, default) global feature_flags_data From 4eeed1a3d0209b88a13d9524e44392b26ef26336 Mon Sep 17 00:00:00 2001 From: Ramon Niebla Date: Tue, 20 Oct 2020 09:09:25 -0700 Subject: [PATCH 04/31] assignment, duh --- rollbar/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 844f7790..4d1cd45e 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -1621,6 +1621,7 @@ def _wsgi_extract_user_ip(environ): class FeatureFlags(object): def __init__(self, flag_key, default=False): self.flag_key = flag_key + self.default = default def __enter__(self): try: @@ -1629,7 +1630,7 @@ def __enter__(self): log.info('Launch Darkly not available') return - variation = ldclient.get().variation(self.flag_key, {}, default) + variation = ldclient.get().variation(self.flag_key, {}, self.default) global feature_flags_data From 4714ba4281fb71edbcefe90782bb9826abafc6bd Mon Sep 17 00:00:00 2001 From: Ramon Niebla Date: Tue, 20 Oct 2020 09:48:19 -0700 Subject: [PATCH 05/31] debugging --- rollbar/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 4d1cd45e..a4ce04d6 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -704,7 +704,7 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): global feature_flags_data data['feature_flags'] = feature_flags_data - feature_flags_data = {} + # feature_flags_data = {} request = _get_actual_request(request) _add_request_data(data, request) @@ -794,7 +794,7 @@ def _report_message(message, level, request, extra_data, payload_data): global feature_flags_data data['feature_flags'] = feature_flags_data - feature_flags_data = {} + # feature_flags_data = {} if payload_data: data = dict_merge(data, payload_data, silence_errors=True) From 021661d542a4f6a0273c688f7f5b2750304082f9 Mon Sep 17 00:00:00 2001 From: Ramon Niebla Date: Tue, 20 Oct 2020 09:56:10 -0700 Subject: [PATCH 06/31] dont delete feature flag data if exception was raise --- rollbar/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index a4ce04d6..2ed2afa3 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -704,7 +704,7 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): global feature_flags_data data['feature_flags'] = feature_flags_data - # feature_flags_data = {} + feature_flags_data = {} request = _get_actual_request(request) _add_request_data(data, request) @@ -794,7 +794,7 @@ def _report_message(message, level, request, extra_data, payload_data): global feature_flags_data data['feature_flags'] = feature_flags_data - # feature_flags_data = {} + feature_flags_data = {} if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1642,6 +1642,9 @@ def __enter__(self): return variation def __exit__(self, type, value, traceback): + if issubclass(type, Exception): + return + global feature_flags_data del feature_flags_data[self.flag_key] From 2401f7873fb8f0cb3d7c4bf0f46dba5fd5cba511 Mon Sep 17 00:00:00 2001 From: Ramon Niebla Date: Tue, 20 Oct 2020 10:31:51 -0700 Subject: [PATCH 07/31] some quick refactoring to flush data when getting out of scope --- rollbar/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 2ed2afa3..8599ab93 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -704,7 +704,6 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): global feature_flags_data data['feature_flags'] = feature_flags_data - feature_flags_data = {} request = _get_actual_request(request) _add_request_data(data, request) @@ -794,7 +793,6 @@ def _report_message(message, level, request, extra_data, payload_data): global feature_flags_data data['feature_flags'] = feature_flags_data - feature_flags_data = {} if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1633,10 +1631,11 @@ def __enter__(self): variation = ldclient.get().variation(self.flag_key, {}, self.default) global feature_flags_data - - feature_flags_data[self.flag_key] = { - 'variation': variation, - # maybe we could use some more data, leaving as placeholder + feature_flags_data = { + self.flag_key: { + 'variation': variation, + # placeholder for more data + } } return variation From 00e4967e4546f0ab7f1a5de7317dc10225bada9f Mon Sep 17 00:00:00 2001 From: Ramon Niebla Date: Tue, 20 Oct 2020 10:39:36 -0700 Subject: [PATCH 08/31] adding user default params --- rollbar/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 8599ab93..8167f9b2 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -1617,8 +1617,12 @@ def _wsgi_extract_user_ip(environ): class FeatureFlags(object): - def __init__(self, flag_key, default=False): + def __init__(self, flag_key, user=None, default=False): + if not user: + user = {} + self.flag_key = flag_key + self.user = user self.default = default def __enter__(self): @@ -1628,7 +1632,8 @@ def __enter__(self): log.info('Launch Darkly not available') return - variation = ldclient.get().variation(self.flag_key, {}, self.default) + # use get to retrieve ld singleton + variation = ldclient.get().variation(self.flag_key, self.user, self.default) global feature_flags_data feature_flags_data = { From 9c51dccf65bc8bef1b87bd7058acc8dd6ab983ae Mon Sep 17 00:00:00 2001 From: atran Date: Wed, 28 Oct 2020 14:55:20 -0400 Subject: [PATCH 09/31] Utilize linked list structure for scope of context manager --- rollbar/__init__.py | 47 +++++++++++++++------------------------------ 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 8167f9b2..eb32f555 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -702,8 +702,8 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if extra_trace_data and not extra_data: data['custom'] = extra_trace_data - global feature_flags_data - data['feature_flags'] = feature_flags_data + if feature_flags_data: + data['feature_flags'] = feature_flags_data request = _get_actual_request(request) _add_request_data(data, request) @@ -791,8 +791,8 @@ def _report_message(message, level, request, extra_data, payload_data): _add_lambda_context_data(data) data['server'] = _build_server_data() - global feature_flags_data - data['feature_flags'] = feature_flags_data + if feature_flags_data: + data['feature_flags'] = feature_flags_data if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1368,7 +1368,8 @@ def _serialize_payload(payload): return json.dumps(payload, default=defaultJSONEncode) -feature_flags_data = {} +feature_flags_data = None + def _send_payload(payload_str, access_token): try: @@ -1617,39 +1618,21 @@ def _wsgi_extract_user_ip(environ): class FeatureFlags(object): - def __init__(self, flag_key, user=None, default=False): - if not user: - user = {} - + def __init__(self, flag_key): self.flag_key = flag_key - self.user = user - self.default = default + self.previous = feature_flags_data def __enter__(self): - try: - import ldclient - except ImportError: - log.info('Launch Darkly not available') - return - - # use get to retrieve ld singleton - variation = ldclient.get().variation(self.flag_key, self.user, self.default) - global feature_flags_data - feature_flags_data = { - self.flag_key: { - 'variation': variation, - # placeholder for more data - } - } - return variation - - def __exit__(self, type, value, traceback): - if issubclass(type, Exception): - return + feature_flags_data = self.flag_key + + return self + def __exit__(self, exc_type, exc_value, traceback): global feature_flags_data - del feature_flags_data[self.flag_key] + + if not exc_type: + feature_flags_data = self.previous From 642cc2094604d409d513c68eefcb3ee8dcd08296 Mon Sep 17 00:00:00 2001 From: atran Date: Wed, 4 Nov 2020 09:35:58 -0500 Subject: [PATCH 10/31] Create watch wrapper method and rename FeatureFlags --- rollbar/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index eb32f555..28972535 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -536,6 +536,10 @@ def wait(f=None): return f() +def watch(flag_key): + return _FeatureFlagContextManager(flag_key) + + class ApiException(Exception): """ This exception will be raised if there was a problem decoding the @@ -1617,7 +1621,7 @@ def _wsgi_extract_user_ip(environ): return environ['REMOTE_ADDR'] -class FeatureFlags(object): +class _FeatureFlagContextManager(object): def __init__(self, flag_key): self.flag_key = flag_key self.previous = feature_flags_data @@ -1634,5 +1638,3 @@ def __exit__(self, exc_type, exc_value, traceback): if not exc_type: feature_flags_data = self.previous - - From ea80652257e6f8352d2f026adeb41cc7e3fd0c87 Mon Sep 17 00:00:00 2001 From: atran Date: Wed, 4 Nov 2020 09:36:54 -0500 Subject: [PATCH 11/31] Revert renamte of FeatureFlags --- rollbar/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 28972535..003264a2 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -537,7 +537,7 @@ def wait(f=None): def watch(flag_key): - return _FeatureFlagContextManager(flag_key) + return FeatureFlags(flag_key) class ApiException(Exception): @@ -1621,7 +1621,7 @@ def _wsgi_extract_user_ip(environ): return environ['REMOTE_ADDR'] -class _FeatureFlagContextManager(object): +class FeatureFlags(object): def __init__(self, flag_key): self.flag_key = flag_key self.previous = feature_flags_data From 83b77b5341aaabb437ccae2dce427fcdfc386a15 Mon Sep 17 00:00:00 2001 From: atran Date: Fri, 6 Nov 2020 15:05:54 -0500 Subject: [PATCH 12/31] Generalize FeatureFlags context manager - use `watch` method wrapper - rename 'FeatureFlags' to '_BigBrother' - implement ability to add 'extra_data' - rename payload key to 'scope' --- rollbar/__init__.py | 59 ++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 003264a2..9f9c22c5 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -536,8 +536,20 @@ def wait(f=None): return f() -def watch(flag_key): - return FeatureFlags(flag_key) +def watch(key, extra_data=None): + """ + Sets the scope for the code wrapped by this method. + + key: key for the current scope. + extra_data: optional, will be included in the root level of the + `scope` object. If not a dict, key will be 'extra'. + + Usage: + + with Rollbar.watch('foobar'): + do_something_risky() + """ + return _BigBrother(key, extra_data) class ApiException(Exception): @@ -706,8 +718,8 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if extra_trace_data and not extra_data: data['custom'] = extra_trace_data - if feature_flags_data: - data['feature_flags'] = feature_flags_data + if scopes: + data['scope'] = scopes[-1] request = _get_actual_request(request) _add_request_data(data, request) @@ -795,8 +807,8 @@ def _report_message(message, level, request, extra_data, payload_data): _add_lambda_context_data(data) data['server'] = _build_server_data() - if feature_flags_data: - data['feature_flags'] = feature_flags_data + if scopes: + data['scope'] = scopes[-1] if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1372,9 +1384,6 @@ def _serialize_payload(payload): return json.dumps(payload, default=defaultJSONEncode) -feature_flags_data = None - - def _send_payload(payload_str, access_token): try: _post_api('item/', payload_str, access_token=access_token) @@ -1621,20 +1630,32 @@ def _wsgi_extract_user_ip(environ): return environ['REMOTE_ADDR'] -class FeatureFlags(object): - def __init__(self, flag_key): - self.flag_key = flag_key - self.previous = feature_flags_data +scopes = [] + + +class _BigBrother(object): + """ + Context manager object that interfaces with the `scopes` stack: - def __enter__(self): - global feature_flags_data + On enter, puts current scope object on top of stack. + On exit and there is no exception, pops off top element from stack. + """ + def __init__(self, key, extra_data): + scope = {'key': key} - feature_flags_data = self.flag_key + if extra_data: + extra_data = extra_data + if not isinstance(extra_data, dict): + extra_data = {'extra': extra_data} + scope = dict_merge(scope, extra_data, silence_errors=True) + + self.scope = scope + + def __enter__(self): + scopes.append(self.scope) return self def __exit__(self, exc_type, exc_value, traceback): - global feature_flags_data - if not exc_type: - feature_flags_data = self.previous + scopes.pop() From 9e4c34af679a4246ddf9d1c5a152a67ab8832b74 Mon Sep 17 00:00:00 2001 From: atran Date: Mon, 9 Nov 2020 09:29:52 -0500 Subject: [PATCH 13/31] Remove unnecessary re-assignment --- rollbar/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 9f9c22c5..a5f8bfd2 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -1644,7 +1644,6 @@ def __init__(self, key, extra_data): scope = {'key': key} if extra_data: - extra_data = extra_data if not isinstance(extra_data, dict): extra_data = {'extra': extra_data} From 5b63fc01fbeb9f90f4ca98eff8904b57283cdb5e Mon Sep 17 00:00:00 2001 From: atran Date: Mon, 9 Nov 2020 13:29:47 -0500 Subject: [PATCH 14/31] Clear out scopes after report_exc_info is called --- rollbar/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index a5f8bfd2..af449c06 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -541,8 +541,9 @@ def watch(key, extra_data=None): Sets the scope for the code wrapped by this method. key: key for the current scope. - extra_data: optional, will be included in the root level of the - `scope` object. If not a dict, key will be 'extra'. + extra_data: optional, will be included in the root level of the 'scope' object. If not + a dict, the value can be found under the 'extra' key in the 'scope' object. + The keyword 'key' is reserved. Usage: @@ -720,7 +721,10 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if scopes: data['scope'] = scopes[-1] - + + global scopes + scopes = [] + request = _get_actual_request(request) _add_request_data(data, request) _add_person_data(data, request) @@ -1647,7 +1651,8 @@ def __init__(self, key, extra_data): if not isinstance(extra_data, dict): extra_data = {'extra': extra_data} - scope = dict_merge(scope, extra_data, silence_errors=True) + extra_data.update(scope) + scope = extra_data self.scope = scope From ae692bfdc0609c3bb72d9f408ea66e3bf163932c Mon Sep 17 00:00:00 2001 From: atran Date: Tue, 10 Nov 2020 23:19:24 -0500 Subject: [PATCH 15/31] Clear scope on exit only --- rollbar/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index af449c06..362b012d 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -722,9 +722,6 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if scopes: data['scope'] = scopes[-1] - global scopes - scopes = [] - request = _get_actual_request(request) _add_request_data(data, request) _add_person_data(data, request) @@ -1661,5 +1658,4 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): - if not exc_type: - scopes.pop() + scopes.pop() From fa6cf69bb420285e6c15d87bdf5c2c9d126d0104 Mon Sep 17 00:00:00 2001 From: atran Date: Wed, 11 Nov 2020 16:35:47 -0500 Subject: [PATCH 16/31] Change reference to scope to tag and force extra_data to be a dict --- rollbar/__init__.py | 46 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 362b012d..8e472c5b 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -536,21 +536,20 @@ def wait(f=None): return f() -def watch(key, extra_data=None): +def watch(tag_name, extra_data=None): """ - Sets the scope for the code wrapped by this method. + Sets the tag for the code wrapped by this method. - key: key for the current scope. - extra_data: optional, will be included in the root level of the 'scope' object. If not - a dict, the value can be found under the 'extra' key in the 'scope' object. - The keyword 'key' is reserved. + tag_name: name of the tag. + extra_data: optional, dictionary of params will be included in the 'tag' object. + 'name' is reserved for the tag_name. Usage: with Rollbar.watch('foobar'): do_something_risky() """ - return _BigBrother(key, extra_data) + return _TagManager(tag_name, extra_data) class ApiException(Exception): @@ -719,8 +718,8 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if extra_trace_data and not extra_data: data['custom'] = extra_trace_data - if scopes: - data['scope'] = scopes[-1] + if tags: + data['tag'] = tags[-1] request = _get_actual_request(request) _add_request_data(data, request) @@ -808,8 +807,8 @@ def _report_message(message, level, request, extra_data, payload_data): _add_lambda_context_data(data) data['server'] = _build_server_data() - if scopes: - data['scope'] = scopes[-1] + if tags: + data['tag'] = tags[-1] if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1631,31 +1630,30 @@ def _wsgi_extract_user_ip(environ): return environ['REMOTE_ADDR'] -scopes = [] +tags = [] -class _BigBrother(object): +class _TagManager(object): """ - Context manager object that interfaces with the `scopes` stack: + Context manager object that interfaces with the `tags` stack: - On enter, puts current scope object on top of stack. - On exit and there is no exception, pops off top element from stack. + On enter, puts current tag object at top of the stack. + On exit, pops off top element of the stack. """ - def __init__(self, key, extra_data): - scope = {'key': key} + def __init__(self, name, extra_data): + self.tag = {} if extra_data: if not isinstance(extra_data, dict): - extra_data = {'extra': extra_data} + raise TypeError('expected \'extra_data\' to be a dictonary') - extra_data.update(scope) - scope = extra_data + self.tag.update(extra_data) - self.scope = scope + self.tag['name'] = name def __enter__(self): - scopes.append(self.scope) + tags.append(self.tag) return self def __exit__(self, exc_type, exc_value, traceback): - scopes.pop() + tags.pop() From e7c0b5c64ec9ab16007fcc1d6be9b302a8bef95e Mon Sep 17 00:00:00 2001 From: atran Date: Thu, 12 Nov 2020 14:44:33 -0500 Subject: [PATCH 17/31] Attach the tag to the exception on exit --- rollbar/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 8e472c5b..f3b112ff 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -718,7 +718,14 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if extra_trace_data and not extra_data: data['custom'] = extra_trace_data - if tags: + attached_tags = getattr(exc_info[1], '_rollbar_tags', None) + if attached_tags: + # if there are tags attached to the exception, take the first one as that's + # the deepest tag where the exception occurred + data['tag'] = attached_tags[0] + elif tags: + # if there are no tags attached to the exception and there are `tags`, that + # means we haven't exited yet - in this case, take the top tag of the stack. data['tag'] = tags[-1] request = _get_actual_request(request) @@ -1639,6 +1646,7 @@ class _TagManager(object): On enter, puts current tag object at top of the stack. On exit, pops off top element of the stack. + - If there is an exception, append the tag to exception._rollbar_tags. """ def __init__(self, name, extra_data): self.tag = {} @@ -1656,4 +1664,10 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): + if exc_value: + if not hasattr(exc_value, '_rollbar_tags'): + exc_value._rollbar_tags = [] + + exc_value._rollbar_tags.append(self.tag) + tags.pop() From 8d8988872854119831d2ee5b5b53107201f5f33f Mon Sep 17 00:00:00 2001 From: atran Date: Mon, 16 Nov 2020 18:32:41 -0600 Subject: [PATCH 18/31] Refactor tags check --- rollbar/__init__.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index f3b112ff..0a4167f1 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -718,15 +718,13 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if extra_trace_data and not extra_data: data['custom'] = extra_trace_data - attached_tags = getattr(exc_info[1], '_rollbar_tags', None) - if attached_tags: - # if there are tags attached to the exception, take the first one as that's - # the deepest tag where the exception occurred - data['tag'] = attached_tags[0] - elif tags: - # if there are no tags attached to the exception and there are `tags`, that - # means we haven't exited yet - in this case, take the top tag of the stack. - data['tag'] = tags[-1] + # if there are tags attached to the exception, use that; else use _tags on the singleton + tags = getattr(exc_info[1], '_rollbar_tags', None)[::-1] or _tags + if tags: + if 'custom' in data: + data['custom'].update(tags[-1]) + else: + data['custom'] = tags[-1] request = _get_actual_request(request) _add_request_data(data, request) @@ -813,9 +811,9 @@ def _report_message(message, level, request, extra_data, payload_data): _add_person_data(data, request) _add_lambda_context_data(data) data['server'] = _build_server_data() - - if tags: - data['tag'] = tags[-1] + + if _tags: + data['tag'] = _tags[-1] if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1637,13 +1635,13 @@ def _wsgi_extract_user_ip(environ): return environ['REMOTE_ADDR'] -tags = [] +_tags = [] class _TagManager(object): """ Context manager object that interfaces with the `tags` stack: - + On enter, puts current tag object at top of the stack. On exit, pops off top element of the stack. - If there is an exception, append the tag to exception._rollbar_tags. @@ -1657,10 +1655,10 @@ def __init__(self, name, extra_data): self.tag.update(extra_data) - self.tag['name'] = name + self.tag['tag_name'] = name def __enter__(self): - tags.append(self.tag) + _tags.append(self.tag) return self def __exit__(self, exc_type, exc_value, traceback): @@ -1670,4 +1668,4 @@ def __exit__(self, exc_type, exc_value, traceback): exc_value._rollbar_tags.append(self.tag) - tags.pop() + _tags.pop() From 9ca6df6bcecdc669eaa750374308f33e274be9b2 Mon Sep 17 00:00:00 2001 From: atran Date: Mon, 16 Nov 2020 20:47:36 -0600 Subject: [PATCH 19/31] Implement specific feature flags interface --- rollbar/__init__.py | 66 +++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 0a4167f1..09bc665e 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -536,20 +536,19 @@ def wait(f=None): return f() -def watch(tag_name, extra_data=None): +def watch_feature_flag(key): """ - Sets the tag for the code wrapped by this method. + Sets the feature flag payload for the code wrapped by this method. - tag_name: name of the tag. - extra_data: optional, dictionary of params will be included in the 'tag' object. - 'name' is reserved for the tag_name. + key: key of the feature flag. Usage: - with Rollbar.watch('foobar'): + with rollbar.watch_feature_flag('feature_flag_a'): do_something_risky() """ - return _TagManager(tag_name, extra_data) + return _FeatureFlagManager(key) + class ApiException(Exception): @@ -718,13 +717,11 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if extra_trace_data and not extra_data: data['custom'] = extra_trace_data - # if there are tags attached to the exception, use that; else use _tags on the singleton - tags = getattr(exc_info[1], '_rollbar_tags', None)[::-1] or _tags - if tags: - if 'custom' in data: - data['custom'].update(tags[-1]) - else: - data['custom'] = tags[-1] + # if there are feature flags attached to the exception, append that to the feature flags on + # singleton to create the full feature flags stack. + feature_flags = _feature_flags + getattr(exc_info[1], '_rollbar_feature_flags', [])[::-1] + if feature_flags: + data['feature_flags'] = feature_flags request = _get_actual_request(request) _add_request_data(data, request) @@ -812,8 +809,8 @@ def _report_message(message, level, request, extra_data, payload_data): _add_lambda_context_data(data) data['server'] = _build_server_data() - if _tags: - data['tag'] = _tags[-1] + if _feature_flags: + data['feature_flags'] = _feature_flags if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1635,37 +1632,30 @@ def _wsgi_extract_user_ip(environ): return environ['REMOTE_ADDR'] -_tags = [] +_feature_flags = [] -class _TagManager(object): +class _FeatureFlagManager(object): """ - Context manager object that interfaces with the `tags` stack: + Context manager object that interfaces with the `_feature_flags` stack: - On enter, puts current tag object at top of the stack. - On exit, pops off top element of the stack. - - If there is an exception, append the tag to exception._rollbar_tags. + On enter, puts the feature flag object at top of the stack. + On exit, pops off the top element of the stack. + - If there is an exception, attach the feature flag object to the exception + for rebuilding of the `_feature_flags` stack before reporting. """ - def __init__(self, name, extra_data): - self.tag = {} - - if extra_data: - if not isinstance(extra_data, dict): - raise TypeError('expected \'extra_data\' to be a dictonary') - - self.tag.update(extra_data) - - self.tag['tag_name'] = name + def __init__(self, key): + self.feature_flag = {'key': key} def __enter__(self): - _tags.append(self.tag) - return self + _feature_flags.append(self.feature_flag) def __exit__(self, exc_type, exc_value, traceback): + if exc_value: - if not hasattr(exc_value, '_rollbar_tags'): - exc_value._rollbar_tags = [] + if not hasattr(exc_value, '_rollbar_feature_flags'): + exc_value._rollbar_feature_flags = [] - exc_value._rollbar_tags.append(self.tag) + exc_value._rollbar_feature_flags.append(self.feature_flag) - _tags.pop() + _feature_flags.pop() \ No newline at end of file From 48eaa570495f16c79f1746ed3110547ca43ba44c Mon Sep 17 00:00:00 2001 From: atran Date: Tue, 17 Nov 2020 00:08:55 -0800 Subject: [PATCH 20/31] change context manager object to take in dict --- rollbar/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 09bc665e..cc135e7c 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -547,7 +547,9 @@ def watch_feature_flag(key): with rollbar.watch_feature_flag('feature_flag_a'): do_something_risky() """ - return _FeatureFlagManager(key) + feature_flag_obj = {'key': key} + + return _FeatureFlagManager(feature_flag_obj) @@ -1644,8 +1646,8 @@ class _FeatureFlagManager(object): - If there is an exception, attach the feature flag object to the exception for rebuilding of the `_feature_flags` stack before reporting. """ - def __init__(self, key): - self.feature_flag = {'key': key} + def __init__(self, feature_flag_obj): + self.feature_flag = feature_flag_obj def __enter__(self): _feature_flags.append(self.feature_flag) From e5fb9161887f87898756a3b246d5196bce84945a Mon Sep 17 00:00:00 2001 From: atran Date: Tue, 17 Nov 2020 08:04:30 -0800 Subject: [PATCH 21/31] Make feature flags stack thread safe --- rollbar/__init__.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index cc135e7c..ce24061f 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -721,7 +721,7 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): # if there are feature flags attached to the exception, append that to the feature flags on # singleton to create the full feature flags stack. - feature_flags = _feature_flags + getattr(exc_info[1], '_rollbar_feature_flags', [])[::-1] + feature_flags = _feature_flags.value + getattr(exc_info[1], '_rollbar_feature_flags', [])[::-1] if feature_flags: data['feature_flags'] = feature_flags @@ -811,8 +811,8 @@ def _report_message(message, level, request, extra_data, payload_data): _add_lambda_context_data(data) data['server'] = _build_server_data() - if _feature_flags: - data['feature_flags'] = _feature_flags + if _feature_flags.value: + data['feature_flags'] = _feature_flags.value if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1634,7 +1634,23 @@ def _wsgi_extract_user_ip(environ): return environ['REMOTE_ADDR'] -_feature_flags = [] +class _LocalFeatureFlagsStack(object): + def __init__(self): + self._registry = threading.local() + self._registry.feature_flags = [] + + def append(self, value): + self._registry.feature_flags.append(value) + + def pop(self): + self._registry.feature_flags.pop() + + @property + def value(self): + return self._registry.feature_flags + + +_feature_flags = _LocalFeatureFlagsStack() class _FeatureFlagManager(object): From 5bae76e6a04cdc8abe649d9849629505f8554f13 Mon Sep 17 00:00:00 2001 From: atran Date: Tue, 17 Nov 2020 18:56:20 -0800 Subject: [PATCH 22/31] Use tags instead of feature flags --- rollbar/__init__.py | 67 +++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index ce24061f..14be5160 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -536,21 +536,19 @@ def wait(f=None): return f() -def watch_feature_flag(key): +def watch(key, value): """ - Sets the feature flag payload for the code wrapped by this method. + Sets a tag for the code wrapped by this method. - key: key of the feature flag. + key: key to look up the tag. + value: value associated with tag. Usage: - with rollbar.watch_feature_flag('feature_flag_a'): + with rollbar.watch('feature_flag', 'feature_flag_a'): do_something_risky() """ - feature_flag_obj = {'key': key} - - return _FeatureFlagManager(feature_flag_obj) - + return _TagManager({'key': key, 'value': value}) class ApiException(Exception): @@ -719,11 +717,11 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if extra_trace_data and not extra_data: data['custom'] = extra_trace_data - # if there are feature flags attached to the exception, append that to the feature flags on - # singleton to create the full feature flags stack. - feature_flags = _feature_flags.value + getattr(exc_info[1], '_rollbar_feature_flags', [])[::-1] - if feature_flags: - data['feature_flags'] = feature_flags + # if there are tags attached to the exception, reverse the order, and append to `_tags` + # stack to send the full chain of tags to Rollbar. + tags = _tags.value + getattr(exc_info[1], '_rollbar_tags', [])[::-1] + if tags: + data['tags'] = tags request = _get_actual_request(request) _add_request_data(data, request) @@ -811,8 +809,8 @@ def _report_message(message, level, request, extra_data, payload_data): _add_lambda_context_data(data) data['server'] = _build_server_data() - if _feature_flags.value: - data['feature_flags'] = _feature_flags.value + if _tags.value: + data['tags'] = _tags.value if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1634,46 +1632,49 @@ def _wsgi_extract_user_ip(environ): return environ['REMOTE_ADDR'] -class _LocalFeatureFlagsStack(object): +class _LocalTags(object): + """ + An object to ensure thread safety. + """ def __init__(self): self._registry = threading.local() - self._registry.feature_flags = [] + self._registry.tags = [] def append(self, value): - self._registry.feature_flags.append(value) + self._registry.tags.append(value) def pop(self): - self._registry.feature_flags.pop() + self._registry.tags.pop() @property def value(self): - return self._registry.feature_flags + return self._registry.tags -_feature_flags = _LocalFeatureFlagsStack() +_tags = _LocalTags() -class _FeatureFlagManager(object): +class _TagManager(object): """ - Context manager object that interfaces with the `_feature_flags` stack: + Context manager object that interfaces with the `_tags` stack: - On enter, puts the feature flag object at top of the stack. + On enter, puts the tag object at top of the stack. On exit, pops off the top element of the stack. - - If there is an exception, attach the feature flag object to the exception - for rebuilding of the `_feature_flags` stack before reporting. + - If there is an exception, attach the tag object to the exception + for rebuilding of the `_tags` stack before reporting. """ - def __init__(self, feature_flag_obj): - self.feature_flag = feature_flag_obj + def __init__(self, tag): + self.tag = tag def __enter__(self): - _feature_flags.append(self.feature_flag) + _tags.append(self.tag) def __exit__(self, exc_type, exc_value, traceback): if exc_value: - if not hasattr(exc_value, '_rollbar_feature_flags'): - exc_value._rollbar_feature_flags = [] + if not hasattr(exc_value, '_rollbar_tags'): + exc_value._rollbar_tags = [] - exc_value._rollbar_feature_flags.append(self.feature_flag) + exc_value._rollbar_tags.append(self.tag) - _feature_flags.pop() \ No newline at end of file + _tags.pop() From 737883512476d4faa99e48ea20b36988f5dca55c Mon Sep 17 00:00:00 2001 From: atran Date: Wed, 18 Nov 2020 09:05:00 -0800 Subject: [PATCH 23/31] Add check for attr --- rollbar/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 14be5160..6a657d74 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -1648,7 +1648,10 @@ def pop(self): @property def value(self): - return self._registry.tags + if hasattr(self._registry, 'tags'): + return self._registry.tags + + return [] _tags = _LocalTags() From 69b035e13625762bfcaa74a44f3cea5e2241686b Mon Sep 17 00:00:00 2001 From: atran Date: Fri, 20 Nov 2020 09:34:38 -0800 Subject: [PATCH 24/31] Model feature flags with tags - remove watch method - utilize feature_flag method that models feature flags as tags --- rollbar/__init__.py | 56 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 6a657d74..371cfdee 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -536,19 +536,47 @@ def wait(f=None): return f() -def watch(key, value): +def feature_flag(flag_key, variation=None, user=None): """ - Sets a tag for the code wrapped by this method. + A context manager interface that sets the tags used to model a feature flag. - key: key to look up the tag. - value: value associated with tag. + key: the key of the feature flag. + variation: (optional) the evaluated feature flag variation. + user: (optional) the user being evaluated. - Usage: + Example usage: + + with rollbar.featureflag('flag1', variation=True, user='foobar@rollbar.com'): + code() + + Tags generated from the above example: - with rollbar.watch('feature_flag', 'feature_flag_a'): - do_something_risky() + [ + {'key': feature_flag.key', 'value': 'flag1'}, + {'key': feature_flag.data.flag1.order', 'value': 0}, + {'key': feature_flag.data.flag1.variation', 'value': True}, + {'key': feature_flag.data.flag1.user, 'value': 'foobar@rollbar.com'} + ] """ - return _TagManager({'key': key, 'value': value}) + flag_key_tag = _create_tag('feature_flag.key', flag_key) + + # create the feature flag order tag + order_key = _feature_flag_data_key(flag_key, 'order') + order_tag = _create_tag(order_key, len(_tags.value)) + + tags = [flag_key_tag, order_tag] + + if variation is not None: + variation_key = _feature_flag_data_key(flag_key, 'variation') + variation_tag = _create_tag(variation_key, variation) + tags.append(variation_tag) + + if user is not None: + user_key = _feature_flag_data_key(flag_key, 'user') + user_tag = _create_tag(user_key, user) + tags.append(user_tag) + + return _TagManager(tags) class ApiException(Exception): @@ -721,7 +749,7 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): # stack to send the full chain of tags to Rollbar. tags = _tags.value + getattr(exc_info[1], '_rollbar_tags', [])[::-1] if tags: - data['tags'] = tags + data['tags'] = _flatten_nested_lists(tags) request = _get_actual_request(request) _add_request_data(data, request) @@ -810,7 +838,7 @@ def _report_message(message, level, request, extra_data, payload_data): data['server'] = _build_server_data() if _tags.value: - data['tags'] = _tags.value + data['tags'] = _flatten_nested_lists(_tags.value) if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1632,6 +1660,14 @@ def _wsgi_extract_user_ip(environ): return environ['REMOTE_ADDR'] +def _create_tag(key, value): + return {'key': key, 'value': value} + + +def _feature_flag_data_key(flag_key, attribute): + return 'feature_flag.data.%s.%s' % (flag_key, attribute) + + class _LocalTags(object): """ An object to ensure thread safety. From 430fd2903d046664727f81db695c5936bac3aaa7 Mon Sep 17 00:00:00 2001 From: atran Date: Tue, 24 Nov 2020 11:28:22 -0800 Subject: [PATCH 25/31] Fix typo in comment --- rollbar/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 371cfdee..49c58104 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -546,7 +546,7 @@ def feature_flag(flag_key, variation=None, user=None): Example usage: - with rollbar.featureflag('flag1', variation=True, user='foobar@rollbar.com'): + with rollbar.feature_flag('flag1', variation=True, user='foobar@rollbar.com'): code() Tags generated from the above example: From 5eb2da2a1b876aefbea4e9c7289ac0391862a9d7 Mon Sep 17 00:00:00 2001 From: atran Date: Tue, 24 Nov 2020 18:05:30 -0800 Subject: [PATCH 26/31] Add test cases for the feature_flag context manager --- .../test/test_feature_flag_context_manager.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 rollbar/test/test_feature_flag_context_manager.py diff --git a/rollbar/test/test_feature_flag_context_manager.py b/rollbar/test/test_feature_flag_context_manager.py new file mode 100644 index 00000000..820fbe99 --- /dev/null +++ b/rollbar/test/test_feature_flag_context_manager.py @@ -0,0 +1,135 @@ +import copy + +try: + from unittest import mock +except ImportError: + import mock + +import rollbar + +from rollbar.test import BaseTest + + +_test_access_token = 'aaaabbbbccccddddeeeeffff00001111' +_default_settings = copy.deepcopy(rollbar.SETTINGS) + + +class FeatureFlagContextManagerTest(BaseTest): + def setUp(self): + rollbar._initialized = False + rollbar.SETTINGS = copy.deepcopy(_default_settings) + rollbar.init(_test_access_token, locals={'enabled': True}, handler='blocking', timeout=12345) + + def test_feature_flag_generates_correct_tag_payload(self): + cm = rollbar.feature_flag('feature-foo', variation=True, user='atran@rollbar.com') + + tags = cm.tag + self.assertEqual(len(tags), 4) + + key, order, variation, user = tags + + self.assertEqual(key['key'], 'feature_flag.key') + self.assertEqual(key['value'], 'feature-foo') + + self.assertEqual(order['key'], 'feature_flag.data.feature-foo.order') + self.assertEqual(order['value'], 0) + + self.assertEqual(variation['key'], 'feature_flag.data.feature-foo.variation') + self.assertEqual(variation['value'], True) + + self.assertEqual(user['key'], 'feature_flag.data.feature-foo.user') + self.assertEqual(user['value'], 'atran@rollbar.com') + + @mock.patch('rollbar.send_payload') + def test_report_message_inside_feature_flag_context_manager(self, send_payload): + with rollbar.feature_flag('feature-foo'): + rollbar.report_message('hello world') + + self.assertEqual(send_payload.called, True) + + payload_data = send_payload.call_args.args[0]['data'] + self.assertIn('tags', payload_data) + + tags = payload_data['tags'] + self.assertEquals(len(tags), 2) + + self._assert_tag_equals(tags[0], 'feature_flag.key', 'feature-foo') + self._assert_tag_equals(tags[1], 'feature_flag.data.feature-foo.order', 0) + + self._report_message_and_assert_no_tags(send_payload) + + @mock.patch('rollbar.send_payload') + def test_report_exc_info_inside_feature_flag_context_manager(self, send_payload): + with rollbar.feature_flag('feature-foo'): + try: + raise Exception('foo') + except: + rollbar.report_exc_info() + + self.assertEqual(send_payload.called, True) + + payload_data = send_payload.call_args.args[0]['data'] + self.assertIn('tags', payload_data) + + tags = payload_data['tags'] + self.assertEquals(len(tags), 2) + + self._assert_tag_equals(tags[0], 'feature_flag.key', 'feature-foo') + self._assert_tag_equals(tags[1], 'feature_flag.data.feature-foo.order', 0) + + self._report_message_and_assert_no_tags(send_payload) + + @mock.patch('rollbar.send_payload') + def test_report_exc_info_outside_feature_flag_context_manager(self, send_payload): + try: + with rollbar.feature_flag('feature-foo'): + raise Exception('foo') + except: + rollbar.report_exc_info() + + self.assertEqual(send_payload.called, True) + + payload_data = send_payload.call_args.args[0]['data'] + self.assertIn('tags', payload_data) + + tags = payload_data['tags'] + self.assertEquals(len(tags), 2) + + self._assert_tag_equals(tags[0], 'feature_flag.key', 'feature-foo') + self._assert_tag_equals(tags[1], 'feature_flag.data.feature-foo.order', 0) + + self._report_message_and_assert_no_tags(send_payload) + + @mock.patch('rollbar.send_payload') + def test_report_exc_info_inside_nested_feature_flag_context_manager(self, send_payload): + try: + with rollbar.feature_flag('feature-foo'): + with rollbar.feature_flag('feature-bar'): + raise Exception('foo') + except: + rollbar.report_exc_info() + + self.assertEqual(send_payload.called, True) + + payload_data = send_payload.call_args.args[0]['data'] + self.assertIn('tags', payload_data) + + tags = payload_data['tags'] + self.assertEquals(len(tags), 4) + + self._assert_tag_equals(tags[0], 'feature_flag.key', 'feature-foo') + self._assert_tag_equals(tags[1], 'feature_flag.data.feature-foo.order', 0) + self._assert_tag_equals(tags[2], 'feature_flag.key', 'feature-bar') + self._assert_tag_equals(tags[3], 'feature_flag.data.feature-bar.order', 1) + + self._report_message_and_assert_no_tags(send_payload) + + def _assert_tag_equals(self, tag, key, value): + self.assertEqual(tag, {'key': key, 'value': value}) + + def _report_message_and_assert_no_tags(self, mocked_send): + rollbar.report_message('this report message is to check that there are no tags') + self.assertEqual(mocked_send.called, True) + + payload_data = mocked_send.call_args.args[0]['data'] + self.assertNotIn('tags', payload_data) From 00c89ecb3a35eaaf8730d02b9d8939c8b8e06d41 Mon Sep 17 00:00:00 2001 From: atran Date: Tue, 24 Nov 2020 18:28:26 -0800 Subject: [PATCH 27/31] Use [0][0] to index mocked object --- rollbar/test/test_feature_flag_context_manager.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/rollbar/test/test_feature_flag_context_manager.py b/rollbar/test/test_feature_flag_context_manager.py index 820fbe99..cd2c7fa5 100644 --- a/rollbar/test/test_feature_flag_context_manager.py +++ b/rollbar/test/test_feature_flag_context_manager.py @@ -47,7 +47,9 @@ def test_report_message_inside_feature_flag_context_manager(self, send_payload): self.assertEqual(send_payload.called, True) - payload_data = send_payload.call_args.args[0]['data'] + # [0][0] is used here to index into the mocked objects `call_args` and get the + # right payload for comparison. + payload_data = send_payload.call_args[0][0]['data'] self.assertIn('tags', payload_data) tags = payload_data['tags'] @@ -68,7 +70,7 @@ def test_report_exc_info_inside_feature_flag_context_manager(self, send_payload) self.assertEqual(send_payload.called, True) - payload_data = send_payload.call_args.args[0]['data'] + payload_data = send_payload.call_args[0][0]['data'] self.assertIn('tags', payload_data) tags = payload_data['tags'] @@ -89,7 +91,7 @@ def test_report_exc_info_outside_feature_flag_context_manager(self, send_payload self.assertEqual(send_payload.called, True) - payload_data = send_payload.call_args.args[0]['data'] + payload_data = send_payload.call_args[0][0]['data'] self.assertIn('tags', payload_data) tags = payload_data['tags'] @@ -111,7 +113,7 @@ def test_report_exc_info_inside_nested_feature_flag_context_manager(self, send_p self.assertEqual(send_payload.called, True) - payload_data = send_payload.call_args.args[0]['data'] + payload_data = send_payload.call_args[0][0]['data'] self.assertIn('tags', payload_data) tags = payload_data['tags'] @@ -131,5 +133,5 @@ def _report_message_and_assert_no_tags(self, mocked_send): rollbar.report_message('this report message is to check that there are no tags') self.assertEqual(mocked_send.called, True) - payload_data = mocked_send.call_args.args[0]['data'] + payload_data = mocked_send.call_args[0][0]['data'] self.assertNotIn('tags', payload_data) From 25045f9dc144e7f1e6b405fd5c1b3c946e527fba Mon Sep 17 00:00:00 2001 From: atran Date: Wed, 25 Nov 2020 15:54:53 -0800 Subject: [PATCH 28/31] Remove feature_flag.data.order --- rollbar/__init__.py | 7 +----- .../test/test_feature_flag_context_manager.py | 22 ++++++------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 49c58104..ceaa2ee0 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -553,18 +553,13 @@ def feature_flag(flag_key, variation=None, user=None): [ {'key': feature_flag.key', 'value': 'flag1'}, - {'key': feature_flag.data.flag1.order', 'value': 0}, {'key': feature_flag.data.flag1.variation', 'value': True}, {'key': feature_flag.data.flag1.user, 'value': 'foobar@rollbar.com'} ] """ flag_key_tag = _create_tag('feature_flag.key', flag_key) - # create the feature flag order tag - order_key = _feature_flag_data_key(flag_key, 'order') - order_tag = _create_tag(order_key, len(_tags.value)) - - tags = [flag_key_tag, order_tag] + tags = [flag_key_tag] if variation is not None: variation_key = _feature_flag_data_key(flag_key, 'variation') diff --git a/rollbar/test/test_feature_flag_context_manager.py b/rollbar/test/test_feature_flag_context_manager.py index cd2c7fa5..7f8f5923 100644 --- a/rollbar/test/test_feature_flag_context_manager.py +++ b/rollbar/test/test_feature_flag_context_manager.py @@ -24,16 +24,13 @@ def test_feature_flag_generates_correct_tag_payload(self): cm = rollbar.feature_flag('feature-foo', variation=True, user='atran@rollbar.com') tags = cm.tag - self.assertEqual(len(tags), 4) + self.assertEqual(len(tags), 3) - key, order, variation, user = tags + key, variation, user = tags self.assertEqual(key['key'], 'feature_flag.key') self.assertEqual(key['value'], 'feature-foo') - self.assertEqual(order['key'], 'feature_flag.data.feature-foo.order') - self.assertEqual(order['value'], 0) - self.assertEqual(variation['key'], 'feature_flag.data.feature-foo.variation') self.assertEqual(variation['value'], True) @@ -53,10 +50,9 @@ def test_report_message_inside_feature_flag_context_manager(self, send_payload): self.assertIn('tags', payload_data) tags = payload_data['tags'] - self.assertEquals(len(tags), 2) + self.assertEquals(len(tags), 1) self._assert_tag_equals(tags[0], 'feature_flag.key', 'feature-foo') - self._assert_tag_equals(tags[1], 'feature_flag.data.feature-foo.order', 0) self._report_message_and_assert_no_tags(send_payload) @@ -74,10 +70,9 @@ def test_report_exc_info_inside_feature_flag_context_manager(self, send_payload) self.assertIn('tags', payload_data) tags = payload_data['tags'] - self.assertEquals(len(tags), 2) + self.assertEquals(len(tags), 1) self._assert_tag_equals(tags[0], 'feature_flag.key', 'feature-foo') - self._assert_tag_equals(tags[1], 'feature_flag.data.feature-foo.order', 0) self._report_message_and_assert_no_tags(send_payload) @@ -95,10 +90,9 @@ def test_report_exc_info_outside_feature_flag_context_manager(self, send_payload self.assertIn('tags', payload_data) tags = payload_data['tags'] - self.assertEquals(len(tags), 2) + self.assertEquals(len(tags), 1) self._assert_tag_equals(tags[0], 'feature_flag.key', 'feature-foo') - self._assert_tag_equals(tags[1], 'feature_flag.data.feature-foo.order', 0) self._report_message_and_assert_no_tags(send_payload) @@ -117,12 +111,10 @@ def test_report_exc_info_inside_nested_feature_flag_context_manager(self, send_p self.assertIn('tags', payload_data) tags = payload_data['tags'] - self.assertEquals(len(tags), 4) + self.assertEquals(len(tags), 2) self._assert_tag_equals(tags[0], 'feature_flag.key', 'feature-foo') - self._assert_tag_equals(tags[1], 'feature_flag.data.feature-foo.order', 0) - self._assert_tag_equals(tags[2], 'feature_flag.key', 'feature-bar') - self._assert_tag_equals(tags[3], 'feature_flag.data.feature-bar.order', 1) + self._assert_tag_equals(tags[1], 'feature_flag.key', 'feature-bar') self._report_message_and_assert_no_tags(send_payload) From bcece26331897eedde77a16dacae9ba412e3b527 Mon Sep 17 00:00:00 2001 From: atran Date: Mon, 30 Nov 2020 11:32:30 -0800 Subject: [PATCH 29/31] use self.tags rather than self.tag --- rollbar/__init__.py | 12 ++++++------ rollbar/test/test_feature_flag_context_manager.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index ceaa2ee0..e26bc86c 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -1692,16 +1692,16 @@ class _TagManager(object): """ Context manager object that interfaces with the `_tags` stack: - On enter, puts the tag object at top of the stack. + On enter, puts the tags at top of the stack. On exit, pops off the top element of the stack. - - If there is an exception, attach the tag object to the exception + - If there is an exception, attach the tags to the exception for rebuilding of the `_tags` stack before reporting. """ - def __init__(self, tag): - self.tag = tag + def __init__(self, tags): + self.tags = tags def __enter__(self): - _tags.append(self.tag) + _tags.append(self.tags) def __exit__(self, exc_type, exc_value, traceback): @@ -1709,6 +1709,6 @@ def __exit__(self, exc_type, exc_value, traceback): if not hasattr(exc_value, '_rollbar_tags'): exc_value._rollbar_tags = [] - exc_value._rollbar_tags.append(self.tag) + exc_value._rollbar_tags.append(self.tags) _tags.pop() diff --git a/rollbar/test/test_feature_flag_context_manager.py b/rollbar/test/test_feature_flag_context_manager.py index 7f8f5923..b549b29d 100644 --- a/rollbar/test/test_feature_flag_context_manager.py +++ b/rollbar/test/test_feature_flag_context_manager.py @@ -23,7 +23,7 @@ def setUp(self): def test_feature_flag_generates_correct_tag_payload(self): cm = rollbar.feature_flag('feature-foo', variation=True, user='atran@rollbar.com') - tags = cm.tag + tags = cm.tags self.assertEqual(len(tags), 3) key, variation, user = tags From 1153529cc6c62480fdf491cb25db94989c3da690 Mon Sep 17 00:00:00 2001 From: atran Date: Mon, 30 Nov 2020 16:56:14 -0800 Subject: [PATCH 30/31] Initialize _tags in thread_local on append and value --- rollbar/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index e26bc86c..0223f729 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -1669,9 +1669,11 @@ class _LocalTags(object): """ def __init__(self): self._registry = threading.local() - self._registry.tags = [] def append(self, value): + if not hasattr(self._registry, 'tags'): + self._registry.tags = [] + self._registry.tags.append(value) def pop(self): @@ -1679,11 +1681,10 @@ def pop(self): @property def value(self): - if hasattr(self._registry, 'tags'): - return self._registry.tags - - return [] + if not hasattr(self._registry, 'tags'): + self._registry.tags = [] + return self._registry.tags _tags = _LocalTags() From d3894db7a750e4a6a0a22de50873e1cf49a7dfed Mon Sep 17 00:00:00 2001 From: atran Date: Thu, 3 Dec 2020 16:49:24 -0800 Subject: [PATCH 31/31] Move flattening to _LocalTags and rename _tags --- rollbar/__init__.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 0223f729..c821f99b 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -538,7 +538,7 @@ def wait(f=None): def feature_flag(flag_key, variation=None, user=None): """ - A context manager interface that sets the tags used to model a feature flag. + A context manager interface that creates a list of tags to represent a feature flag. key: the key of the feature flag. variation: (optional) the evaluated feature flag variation. @@ -740,11 +740,15 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None): if extra_trace_data and not extra_data: data['custom'] = extra_trace_data - # if there are tags attached to the exception, reverse the order, and append to `_tags` - # stack to send the full chain of tags to Rollbar. - tags = _tags.value + getattr(exc_info[1], '_rollbar_tags', [])[::-1] + tags = _in_context_tags.to_list() + + # if there are tags attached to the exception, reverse the order, and append to `tags` + # to send the full chain of tags to Rollbar. + if hasattr(exc_info[1], '_rollbar_tags'): + tags += getattr(exc_info[1], '_rollbar_tags').to_list(reverse=True) + if tags: - data['tags'] = _flatten_nested_lists(tags) + data['tags'] = tags request = _get_actual_request(request) _add_request_data(data, request) @@ -832,8 +836,8 @@ def _report_message(message, level, request, extra_data, payload_data): _add_lambda_context_data(data) data['server'] = _build_server_data() - if _tags.value: - data['tags'] = _flatten_nested_lists(_tags.value) + if _in_context_tags.to_list(): + data['tags'] = _in_context_tags.to_list() if payload_data: data = dict_merge(data, payload_data, silence_errors=True) @@ -1679,37 +1683,40 @@ def append(self, value): def pop(self): self._registry.tags.pop() - @property - def value(self): + def to_list(self, reverse=False): if not hasattr(self._registry, 'tags'): self._registry.tags = [] - return self._registry.tags + if reverse: + return _flatten_nested_lists(self._registry.tags[::-1]) + + return _flatten_nested_lists(self._registry.tags) + -_tags = _LocalTags() +_in_context_tags = _LocalTags() class _TagManager(object): """ - Context manager object that interfaces with the `_tags` stack: + Context manager object that interfaces with the `_in_context_tags` stack: On enter, puts the tags at top of the stack. On exit, pops off the top element of the stack. - If there is an exception, attach the tags to the exception - for rebuilding of the `_tags` stack before reporting. + for rebuilding the tags in the context before reporting. """ def __init__(self, tags): self.tags = tags def __enter__(self): - _tags.append(self.tags) + _in_context_tags.append(self.tags) def __exit__(self, exc_type, exc_value, traceback): if exc_value: if not hasattr(exc_value, '_rollbar_tags'): - exc_value._rollbar_tags = [] + exc_value._rollbar_tags = _LocalTags() exc_value._rollbar_tags.append(self.tags) - _tags.pop() + _in_context_tags.pop()