Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3652ce3
Add new integration and unit tests
aliu39 Dec 4, 2024
625969e
Test flag values for LD and OF threaded/asyncio, not just flag names
aliu39 Dec 4, 2024
beb9512
update ffIntegration test to be e2e, and fix LRU copy bug
aliu39 Dec 5, 2024
296545d
Merge branch 'master' into aliu/ff-integration-and-added-coverage
aliu39 Dec 6, 2024
921b133
make a helper fixture and test error processor in original thread
aliu39 Dec 6, 2024
4651b6a
Move api to top-level, rename to add_flag
aliu39 Dec 6, 2024
36bc869
Add docstrs
aliu39 Dec 6, 2024
81c1761
Rename to add_feature_flag
aliu39 Dec 6, 2024
42f76a3
Rm extra import in test_lru_cache
aliu39 Dec 6, 2024
ef01990
Revert lru comment
aliu39 Dec 6, 2024
381ccc1
Type annotate
aliu39 Dec 6, 2024
c76192e
Review comments
aliu39 Dec 6, 2024
b63982b
Update launchdarkly and openfeature tests to be e2e
aliu39 Dec 6, 2024
af1128c
Merge branch 'master' into aliu/ff-integration-and-added-coverage
aliu39 Dec 16, 2024
4c6f08a
Merge branch 'master' into aliu/ff-integration-and-added-coverage
antonpirker Dec 18, 2024
b4eb421
Update docstrs
aliu39 Dec 18, 2024
043e298
Skip threading test for <3.7
aliu39 Dec 18, 2024
41fdde5
Merge branch 'master' into aliu/ff-integration-and-added-coverage
aliu39 Dec 18, 2024
2dc679d
Skip ffs asyncio test if 3.6
aliu39 Dec 18, 2024
af9e92d
undo 'skip threading test'
aliu39 Dec 18, 2024
2cea37b
Try commenting out asyncio
aliu39 Dec 19, 2024
fea761c
Use importorskip
aliu39 Dec 19, 2024
752ce7d
Import order
aliu39 Dec 19, 2024
d77ebc3
Also use skipif
aliu39 Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions sentry_sdk/_lru_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@

"""

from copy import copy
from copy import copy, deepcopy

SENTINEL = object()

Expand Down Expand Up @@ -92,10 +92,13 @@ def __init__(self, max_size):
self.hits = self.misses = 0

def __copy__(self):
"""
Cache keys and values are shallow copied.
"""
cache = LRUCache(self.max_size)
cache.full = self.full
cache.cache = copy(self.cache)
cache.root = copy(self.root)
cache.root = deepcopy(self.root)
return cache

def set(self, key, value):
Expand Down
18 changes: 18 additions & 0 deletions sentry_sdk/integrations/featureflags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from sentry_sdk.flag_utils import flag_error_processor

import sentry_sdk
from sentry_sdk.integrations import Integration


class FeatureFlagsIntegration(Integration):
identifier = "featureflags"

@staticmethod
def setup_once():
scope = sentry_sdk.get_current_scope()
scope.add_error_processor(flag_error_processor)

@staticmethod
def set_flag(flag: str, result: bool):
flags = sentry_sdk.get_current_scope().flags
flags.set(flag, result)
Empty file.
117 changes: 117 additions & 0 deletions tests/integrations/featureflags/test_featureflags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import asyncio
import concurrent.futures as cf

import sentry_sdk
from sentry_sdk.integrations import _processed_integrations
from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration


def test_featureflags_integration(sentry_init, capture_events):
_processed_integrations.discard(
FeatureFlagsIntegration.identifier
) # force reinstall
sentry_init(integrations=[FeatureFlagsIntegration()])
flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration)

flags_integration.set_flag("hello", False)
flags_integration.set_flag("world", True)
flags_integration.set_flag("other", False)

events = capture_events()
sentry_sdk.capture_exception(Exception("something wrong!"))
[event] = events

assert event["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "world", "result": True},
{"flag": "other", "result": False},
]
}


def test_featureflags_integration_threaded(sentry_init, capture_events):
_processed_integrations.discard(
FeatureFlagsIntegration.identifier
) # force reinstall
sentry_init(integrations=[FeatureFlagsIntegration()])
events = capture_events()

# Capture an eval before we split isolation scopes.
flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration)
flags_integration.set_flag("hello", False)

def task(flag_key):
# Creates a new isolation scope for the thread.
# This means the evaluations in each task are captured separately.
with sentry_sdk.isolation_scope():
flags_integration = sentry_sdk.get_client().get_integration(
FeatureFlagsIntegration
)
flags_integration.set_flag(flag_key, False)
# use a tag to identify to identify events later on
sentry_sdk.set_tag("flag_key", flag_key)
sentry_sdk.capture_exception(Exception("something wrong!"))

# Run tasks in separate threads
with cf.ThreadPoolExecutor(max_workers=2) as pool:
pool.map(task, ["world", "other"])

assert len(events) == 2
events.sort(key=lambda e: e["tags"]["flag_key"])
assert events[0]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "other", "result": False},
]
}
assert events[1]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "world", "result": False},
]
}


def test_featureflags_integration_asyncio(sentry_init, capture_events):
_processed_integrations.discard(
FeatureFlagsIntegration.identifier
) # force reinstall
sentry_init(integrations=[FeatureFlagsIntegration()])
events = capture_events()

# Capture an eval before we split isolation scopes.
flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration)
flags_integration.set_flag("hello", False)

async def task(flag_key):
# Creates a new isolation scope for the thread.
# This means the evaluations in each task are captured separately.
with sentry_sdk.isolation_scope():
flags_integration = sentry_sdk.get_client().get_integration(
FeatureFlagsIntegration
)
flags_integration.set_flag(flag_key, False)
# use a tag to identify to identify events later on
sentry_sdk.set_tag("flag_key", flag_key)
sentry_sdk.capture_exception(Exception("something wrong!"))

async def runner():
return asyncio.gather(task("world"), task("other"))

asyncio.run(runner())

assert len(events) == 2
events.sort(key=lambda e: e["tags"]["flag_key"])
assert events[0]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "other", "result": False},
]
}
assert events[1]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "world", "result": False},
]
}
24 changes: 18 additions & 6 deletions tests/integrations/launchdarkly/test_launchdarkly.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def task(flag_key):
# This means the evaluations in each task are captured separately.
with sentry_sdk.isolation_scope():
client.variation(flag_key, context, False)
return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()]
return sentry_sdk.get_current_scope().flags.get()

td.update(td.flag("hello").variation_for_all(True))
td.update(td.flag("world").variation_for_all(False))
Expand All @@ -67,8 +67,14 @@ def task(flag_key):
with cf.ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(task, ["world", "other"]))

assert results[0] == ["hello", "world"]
assert results[1] == ["hello", "other"]
assert results[0] == [
{"flag": "hello", "result": True},
{"flag": "world", "result": False},
]
assert results[1] == [
{"flag": "hello", "result": True},
{"flag": "other", "result": False},
]


def test_launchdarkly_integration_asyncio(sentry_init):
Expand All @@ -81,7 +87,7 @@ def test_launchdarkly_integration_asyncio(sentry_init):
async def task(flag_key):
with sentry_sdk.isolation_scope():
client.variation(flag_key, context, False)
return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()]
return sentry_sdk.get_current_scope().flags.get()

async def runner():
return asyncio.gather(task("world"), task("other"))
Expand All @@ -91,8 +97,14 @@ async def runner():
client.variation("hello", context, False)

results = asyncio.run(runner()).result()
assert results[0] == ["hello", "world"]
assert results[1] == ["hello", "other"]
assert results[0] == [
{"flag": "hello", "result": True},
{"flag": "world", "result": False},
]
assert results[1] == [
{"flag": "hello", "result": True},
{"flag": "other", "result": False},
]


def test_launchdarkly_integration_did_not_enable(monkeypatch):
Expand Down
24 changes: 18 additions & 6 deletions tests/integrations/openfeature/test_openfeature.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,19 @@ def task(flag):
# Create a new isolation scope for the thread. This means the flags
with sentry_sdk.isolation_scope():
client.get_boolean_value(flag, default_value=False)
return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()]
return sentry_sdk.get_current_scope().flags.get()

with cf.ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(task, ["world", "other"]))

assert results[0] == ["hello", "world"]
assert results[1] == ["hello", "other"]
assert results[0] == [
{"flag": "hello", "result": True},
{"flag": "world", "result": False},
]
assert results[1] == [
{"flag": "hello", "result": True},
{"flag": "other", "result": False},
]


def test_openfeature_integration_asyncio(sentry_init):
Expand All @@ -59,7 +65,7 @@ def test_openfeature_integration_asyncio(sentry_init):
async def task(flag):
with sentry_sdk.isolation_scope():
client.get_boolean_value(flag, default_value=False)
return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()]
return sentry_sdk.get_current_scope().flags.get()

async def runner():
return asyncio.gather(task("world"), task("other"))
Expand All @@ -76,5 +82,11 @@ async def runner():
client.get_boolean_value("hello", default_value=False)

results = asyncio.run(runner()).result()
assert results[0] == ["hello", "world"]
assert results[1] == ["hello", "other"]
assert results[0] == [
{"flag": "hello", "result": True},
{"flag": "world", "result": False},
]
assert results[1] == [
{"flag": "hello", "result": True},
{"flag": "other", "result": False},
]
19 changes: 19 additions & 0 deletions tests/test_lru_cache.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from copy import copy

import pytest

from sentry_sdk._lru_cache import LRUCache
Expand Down Expand Up @@ -58,3 +60,20 @@ def test_cache_get_all():
assert cache.get_all() == [(1, 1), (2, 2), (3, 3)]
cache.get(1)
assert cache.get_all() == [(2, 2), (3, 3), (1, 1)]


def test_cache_copy():
cache = LRUCache(3)
cache.set(0, 0)
cache.set(1, 1)

copied = copy(cache)
cache.set(2, 2)
cache.set(3, 3)
assert copied.get_all() == [(0, 0), (1, 1)]
assert cache.get_all() == [(1, 1), (2, 2), (3, 3)]

copied = copy(cache)
cache.get(1)
assert copied.get_all() == [(1, 1), (2, 2), (3, 3)]
assert cache.get_all() == [(2, 2), (3, 3), (1, 1)]
Loading