-
Notifications
You must be signed in to change notification settings - Fork 510
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add LaunchDarkly and OpenFeature integration (#3648)
Adds LaunchDarkly and OpenFeature integration and extends the `Scope` with a `flags` property. As flags are evaluated by an application they are stored within the Sentry SDK (lru cache). When an error occurs we fetch the flags stored in the SDK and serialize them on the event. --------- Co-authored-by: Anton Pirker <[email protected]> Co-authored-by: Ivana Kellyer <[email protected]> Co-authored-by: Andrew Liu <[email protected]>
- Loading branch information
1 parent
d06a189
commit dd1117d
Showing
18 changed files
with
498 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,3 +15,5 @@ flake8-bugbear | |
pep8-naming | ||
pre-commit # local linting | ||
httpcore | ||
openfeature-sdk | ||
launchdarkly-server-sdk |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
from copy import copy | ||
from typing import TYPE_CHECKING | ||
|
||
import sentry_sdk | ||
from sentry_sdk._lru_cache import LRUCache | ||
|
||
if TYPE_CHECKING: | ||
from typing import TypedDict, Optional | ||
from sentry_sdk._types import Event, ExcInfo | ||
|
||
FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) | ||
|
||
|
||
DEFAULT_FLAG_CAPACITY = 100 | ||
|
||
|
||
class FlagBuffer: | ||
|
||
def __init__(self, capacity): | ||
# type: (int) -> None | ||
self.buffer = LRUCache(capacity) | ||
self.capacity = capacity | ||
|
||
def clear(self): | ||
# type: () -> None | ||
self.buffer = LRUCache(self.capacity) | ||
|
||
def __copy__(self): | ||
# type: () -> FlagBuffer | ||
buffer = FlagBuffer(capacity=self.capacity) | ||
buffer.buffer = copy(self.buffer) | ||
return buffer | ||
|
||
def get(self): | ||
# type: () -> list[FlagData] | ||
return [{"flag": key, "result": value} for key, value in self.buffer.get_all()] | ||
|
||
def set(self, flag, result): | ||
# type: (str, bool) -> None | ||
self.buffer.set(flag, result) | ||
|
||
|
||
def flag_error_processor(event, exc_info): | ||
# type: (Event, ExcInfo) -> Optional[Event] | ||
scope = sentry_sdk.get_current_scope() | ||
event["contexts"]["flags"] = {"values": scope.flags.get()} | ||
return event |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
from typing import TYPE_CHECKING | ||
import sentry_sdk | ||
|
||
from sentry_sdk.integrations import DidNotEnable, Integration | ||
from sentry_sdk.flag_utils import flag_error_processor | ||
|
||
try: | ||
import ldclient | ||
from ldclient.hook import Hook, Metadata | ||
|
||
if TYPE_CHECKING: | ||
from ldclient import LDClient | ||
from ldclient.hook import EvaluationSeriesContext | ||
from ldclient.evaluation import EvaluationDetail | ||
|
||
from typing import Any | ||
except ImportError: | ||
raise DidNotEnable("LaunchDarkly is not installed") | ||
|
||
|
||
class LaunchDarklyIntegration(Integration): | ||
identifier = "launchdarkly" | ||
|
||
def __init__(self, ld_client=None): | ||
# type: (LDClient | None) -> None | ||
""" | ||
:param client: An initialized LDClient instance. If a client is not provided, this | ||
integration will attempt to use the shared global instance. | ||
""" | ||
try: | ||
client = ld_client or ldclient.get() | ||
except Exception as exc: | ||
raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) | ||
|
||
if not client.is_initialized(): | ||
raise DidNotEnable("LaunchDarkly client is not initialized.") | ||
|
||
# Register the flag collection hook with the LD client. | ||
client.add_hook(LaunchDarklyHook()) | ||
|
||
@staticmethod | ||
def setup_once(): | ||
# type: () -> None | ||
scope = sentry_sdk.get_current_scope() | ||
scope.add_error_processor(flag_error_processor) | ||
|
||
|
||
class LaunchDarklyHook(Hook): | ||
|
||
@property | ||
def metadata(self): | ||
# type: () -> Metadata | ||
return Metadata(name="sentry-feature-flag-recorder") | ||
|
||
def after_evaluation(self, series_context, data, detail): | ||
# type: (EvaluationSeriesContext, dict[Any, Any], EvaluationDetail) -> dict[Any, Any] | ||
if isinstance(detail.value, bool): | ||
flags = sentry_sdk.get_current_scope().flags | ||
flags.set(series_context.key, detail.value) | ||
return data | ||
|
||
def before_evaluation(self, series_context, data): | ||
# type: (EvaluationSeriesContext, dict[Any, Any]) -> dict[Any, Any] | ||
return data # No-op. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from typing import TYPE_CHECKING | ||
import sentry_sdk | ||
|
||
from sentry_sdk.integrations import DidNotEnable, Integration | ||
from sentry_sdk.flag_utils import flag_error_processor | ||
|
||
try: | ||
from openfeature import api | ||
from openfeature.hook import Hook | ||
|
||
if TYPE_CHECKING: | ||
from openfeature.flag_evaluation import FlagEvaluationDetails | ||
from openfeature.hook import HookContext, HookHints | ||
except ImportError: | ||
raise DidNotEnable("OpenFeature is not installed") | ||
|
||
|
||
class OpenFeatureIntegration(Integration): | ||
identifier = "openfeature" | ||
|
||
@staticmethod | ||
def setup_once(): | ||
# type: () -> None | ||
scope = sentry_sdk.get_current_scope() | ||
scope.add_error_processor(flag_error_processor) | ||
|
||
# Register the hook within the global openfeature hooks list. | ||
api.add_hooks(hooks=[OpenFeatureHook()]) | ||
|
||
|
||
class OpenFeatureHook(Hook): | ||
|
||
def after(self, hook_context, details, hints): | ||
# type: (HookContext, FlagEvaluationDetails[bool], HookHints) -> None | ||
if isinstance(details.value, bool): | ||
flags = sentry_sdk.get_current_scope().flags | ||
flags.set(details.flag_key, details.value) | ||
|
||
def error(self, hook_context, exception, hints): | ||
# type: (HookContext, Exception, HookHints) -> None | ||
if isinstance(hook_context.default_value, bool): | ||
flags = sentry_sdk.get_current_scope().flags | ||
flags.set(hook_context.flag_key, hook_context.default_value) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import pytest | ||
|
||
pytest.importorskip("ldclient") |
Oops, something went wrong.