Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 5 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand All @@ -24,12 +24,9 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install poetry

- name: Load cached venv
id: cached-poetry-dependencies
Expand All @@ -40,7 +37,7 @@ jobs:

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --with dev
run: poetry install --no-interaction --with dev -E sentry

- name: Black
run: |
Expand Down
397 changes: 269 additions & 128 deletions poetry.lock

Large diffs are not rendered by default.

45 changes: 29 additions & 16 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
[project]
[build-system]
requires = ["poetry-core>=1.8.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "tracker"
version = "0.3.0"
version = "0.4.0"
description = ""
authors = [
{name = "Gabriel de Oliveira Chaves", email = "gabriel.chaves@maistodos.com.br"},
{name = "Johnathan Barbosa de Araujo", email = "johnathan.araujo@maistodos.com.br"}
"Gabriel de Oliveira Chaves <gabriel.chaves@maistodos.com.br>",
"Johnathan Barbosa de Araujo <johnathan.araujo@maistodos.com.br>"
]
readme = "README.md"
requires-python = ">=3.9,<4.0"
dependencies = [
"sentry-sdk (>=2.35.2,<3.0.0)"
]
packages = [{ include = "tracker" }]

[tool.poetry.dependencies]
python = ">=3.8,<4.0"
sentry-sdk = { version = ">=1.20.0,<3.0.0", optional = true }

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.extras]
sentry = ["sentry-sdk"]

[tool.poetry.group.dev.dependencies]
ipdb = "^0.13.13"
pytest = "^8.4.1"
black = "^25.1.0"
isort = "^6.0.1"
coverage = "^7.10.6"
mutmut = "^3.3.1"
mutmut = [
{ version = "3.2.0", markers = "python_version < '3.9'" },
{ version = "^3.3.1", markers = "python_version >= '3.9'" }
]
pytest = [
{ version = "^7.4.4", markers = "python_version < '3.9'" },
{ version = "^8.4.1", markers = "python_version >= '3.9'" }
]
black = "23.9.1"
coverage = "^6.4.6"

isort = [
{ version = "^5.12.0", markers = "python_version < '3.9'" },
{ version = "^6.0.1", markers = "python_version >= '3.9'" }
]

[tool.isort]
multi_line_output = 3
Expand Down
94 changes: 86 additions & 8 deletions tests/providers/sentry/test_core.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
import pytest

from tracker.providers.sentry import SentryCore


def test_sentry_core_with_tracing_enabled(mock_init):
def test_sentry_core_init_without_sentry(monkeypatch):
# Simulate ImportError when sentry_sdk is not installed
monkeypatch.setitem(__import__("sys").modules, "sentry_sdk", None)

with pytest.raises(ImportError) as excinfo:
SentryCore(
SentryCore.SentryConfig(
dsn="http://example.com",
environment="testing",
),
)

error_message = (
"sentry-sdk is required to use Sentry with Tracker."
" Please install it via 'pip install sentry-sdk'."
)
assert error_message == str(excinfo.value)


def test_sentry_core_init_without_otel(mock_init):
before_send_function = lambda event, _: event

SentryCore(
SentryCore.SentryConfig(
dsn="http://example.com",
environment="testing",
traces_sample_rate=1.0,
)
before_send_function=before_send_function,
use_otel=False,
),
)

sentry_logging_integration = mock_init.call_args[1]["integrations"][0]
Expand All @@ -17,29 +42,82 @@ def test_sentry_core_with_tracing_enabled(mock_init):
environment="testing",
integrations=[sentry_logging_integration],
traces_sample_rate=1.0,
enable_tracing=True,
before_send=before_send_function,
instrumenter="sentry",
)


def test_sentry_core_with_tracing_disabled(mock_init):
def test_sentry_core_init_with_otel_without_otel_installed(mock_init):
SentryCore(
SentryCore.SentryConfig(
dsn="http://example.com",
environment="testing",
traces_sample_rate=0.0,
)
traces_sample_rate=1.0,
use_otel=True,
),
)

sentry_logging_integration = mock_init.call_args[1]["integrations"][0]

mock_init.assert_called_once_with(
dsn="http://example.com",
environment="testing",
integrations=[sentry_logging_integration],
traces_sample_rate=0.0,
enable_tracing=False,
traces_sample_rate=1.0,
before_send=None,
instrumenter="sentry",
)


def test_sentry_core_init_with_otel(mock_init):
# Mock OpenTelemetry imports
import sys
from unittest.mock import MagicMock

sys.modules["opentelemetry"] = MagicMock()
sys.modules["opentelemetry.trace"] = MagicMock()
sys.modules["opentelemetry.propagate"] = MagicMock()
sys.modules["sentry_sdk.integrations.opentelemetry"] = MagicMock()

SentryCore(
SentryCore.SentryConfig(
dsn="http://example.com",
environment="testing",
traces_sample_rate=1.0,
use_otel=True,
),
)

# assert OpenTelemetry components were initialized
from opentelemetry import trace
from opentelemetry.propagate import set_global_textmap
from sentry_sdk.integrations.opentelemetry import (
SentryPropagator,
SentrySpanProcessor,
)

trace.get_tracer_provider().add_span_processor.assert_called_once()
set_global_textmap.assert_called_once()
SentrySpanProcessor.assert_called_once()
SentryPropagator.assert_called_once()

sentry_logging_integration = mock_init.call_args[1]["integrations"][0]
mock_init.assert_called_once_with(
dsn="http://example.com",
environment="testing",
integrations=[sentry_logging_integration],
traces_sample_rate=1.0,
before_send=None,
instrumenter="otel",
)

# Clean up mocked modules
del sys.modules["opentelemetry"]
del sys.modules["opentelemetry.trace"]
del sys.modules["opentelemetry.propagate"]
del sys.modules["sentry_sdk.integrations.opentelemetry"]


def test_sentry_core_capture_message(capture_message_mock):
core = SentryCore(
SentryCore.SentryConfig(
Expand Down
2 changes: 0 additions & 2 deletions tests/providers/sentry/test_exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ def test_sentry_exception_handler_capture(
sentry_core_mock,
tracker_exception,
):

exception_handler = SentryExceptionHandler(sentry_core_mock)

exception_handler.set_tags({"global_tag": "global_value"})
Expand All @@ -26,7 +25,6 @@ def test_sentry_exception_handler_capture_with_tags_and_contexts(
sentry_core_mock,
tracker_exception,
):

exception_handler = SentryExceptionHandler(sentry_core_mock)

exception_handler.set_tags({"global_tag": "global_value"})
Expand Down
2 changes: 0 additions & 2 deletions tests/providers/sentry/test_message_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ def test_sentry_message_handler_capture(
sentry_core_mock,
tracker_message,
):

message_handler = SentryMessageHandler(sentry_core_mock)

message_handler.set_tags({"global_tag": "global_value"})
Expand All @@ -26,7 +25,6 @@ def test_sentry_message_handler_capture_with_tags_and_contexts(
sentry_core_mock,
tracker_message,
):

message_handler = SentryMessageHandler(sentry_core_mock)

message_handler.set_tags({"global_tag": "global_value"})
Expand Down
17 changes: 12 additions & 5 deletions tracker/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@

class ISetMixin(ABC):
@abstractmethod
def set_tags(self, tags: Tags): ...
def set_tags(self, tags: Tags): # pragma: no cover
...

@abstractmethod
def set_contexts(self, contexts: Contexts): ...
def set_contexts(self, contexts: Contexts): # pragma: no cover
...


class ITrackerHandlerException(ISetMixin, ABC):
@abstractmethod
def capture_exception(self, tracker_exception: TrackerException): ...
def capture_exception(
self, tracker_exception: TrackerException
): # pragma: no cover
...


class ITrackerHandlerMessage(ISetMixin, ABC):
@abstractmethod
def capture_message(self, tracker_message: TrackerMessage): ...
def capture_message(self, tracker_message: TrackerMessage): # pragma: no cover
...


class ITrackerHandlerEvent(ISetMixin, ABC):
@abstractmethod
def capture_event(self, tracker_event: TrackerEvent): ...
def capture_event(self, tracker_event: TrackerEvent): # pragma: no cover
...
Loading