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
3 changes: 3 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
- name: Run packaging step
env:
UV_PYTHON: "3.12"
ISIC_CLI_SENTRY_DSN: ${{ secrets.ISIC_CLI_SENTRY_DSN }}
run: |
uv run tox -e package

Expand Down Expand Up @@ -68,6 +69,8 @@ jobs:
- name: Run packaging step
run: |
uv run tox -e package
env:
ISIC_CLI_SENTRY_DSN: ${{ secrets.ISIC_CLI_SENTRY_DSN }}
- name: Zip and upload binary
run: |
.github/zip_and_upload_package.sh ${{ runner.os }} ${{ github.event.release.tag_name }}
Expand Down
17 changes: 12 additions & 5 deletions isic_cli/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
from isic_cli.session import get_session
from isic_cli.utils.version import check_for_newer_version, get_version, is_dev_install

# the DSN is kept out of the source. this module only exists in the pyinstaller binaries.
try:
from _isic_cli_sentry_dsn import SENTRY_DSN # pyright: ignore[reportMissingImports]
except ImportError:
SENTRY_DSN: str | None = None

if TYPE_CHECKING:
from collections.abc import Iterator

Expand All @@ -46,8 +52,6 @@
"prod": "https://api.isic-archive.com",
}

SENTRY_DSN = "https://3c3afa5c12e04042979583df1a07abd2@o267860.ingest.sentry.io/6645383"

logger = logging.getLogger("isic_cli")


Expand All @@ -59,7 +63,9 @@ def _sentry_atexit_display(pending: int, timeout: int) -> None:


def _sentry_setup():
if not is_dev_install():
# don't pass a missing dsn to sentry_sdk.init, it would fall back to the generic SENTRY_DSN
# environment variable which may belong to an unrelated project.
if SENTRY_DSN and not is_dev_install():
sentry_sdk.init(
SENTRY_DSN,
release=str(get_version()),
Expand Down Expand Up @@ -245,9 +251,10 @@ def _report_unexpected_errors(ctx: click.Context) -> Iterator[None]:
sys.exit(1)

# the prompt can't be answered without an interactive terminal (e.g. cron, CI, or piped
# input), so only point to the issue tracker.
# input), and a bug report can't be sent without a DSN (e.g. a pip install), so only
# point to the issue tracker.
send_bug_report = "n"
if sys.stdin.isatty():
if sys.stdin.isatty() and sentry_sdk.is_initialized():
send_bug_report = click.prompt(
click.style(
"This is a bug in isic-cli, would you like to send a bug report?", fg="yellow"
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies = [
"requests",
"retryable-requests",
"rich",
"sentry-sdk",
"sentry-sdk>=2.0.0",
"tenacity",
]
dynamic = ["version"]
Expand Down Expand Up @@ -115,6 +115,9 @@ ignore = [
]

[tool.ruff.lint.per-file-ignores]
"scripts/**" = [
"INP001", # File is part of an implicit namespace package
]
"tests/**" = [
"PLR0913", # Too many arguments to function call
"PLR2004", # Magic value used in comparison
Expand Down
24 changes: 24 additions & 0 deletions scripts/write_sentry_dsn_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Write a module that embeds the Sentry DSN into the PyInstaller binary."""

from __future__ import annotations

import os
from pathlib import Path
import sys

SENTRY_DSN_ENV_VAR = "ISIC_CLI_SENTRY_DSN"


def main():
module_path = Path(sys.argv[1])
dsn = os.environ.get(SENTRY_DSN_ENV_VAR)

if not dsn:
raise SystemExit(f"{SENTRY_DSN_ENV_VAR} must be set to embed it in the binary.")

module_path.parent.mkdir(parents=True, exist_ok=True)
module_path.write_text(f"SENTRY_DSN = {dsn!r}\n")


if __name__ == "__main__":
main()
16 changes: 10 additions & 6 deletions tests/test_cli_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,17 @@ def test_new_version(


@pytest.mark.parametrize(
("interactive", "send_bug_report", "capture_exception_sent"),
("interactive", "sentry_initialized", "send_bug_report", "capture_exception_sent"),
[
(True, "y", 1),
(True, "n", 0),
(False, None, 0),
(True, True, "y", 1),
(True, True, "n", 0),
(False, True, None, 0),
(True, False, None, 0),
],
)
def test_sentry_error_capture(mocker, capsys, interactive, send_bug_report, capture_exception_sent):
def test_sentry_error_capture(
mocker, capsys, interactive, sentry_initialized, send_bug_report, capture_exception_sent
):
# Note: _sentry_setup is always mocked
from isic_cli import cli
from isic_cli.cli import main
Expand All @@ -59,13 +62,14 @@ def test_sentry_error_capture(mocker, capsys, interactive, send_bug_report, capt
mocker.patch("isic_cli.cli.is_dev_install", return_value=False)
stdin = mocker.patch.object(sys, "stdin")
stdin.isatty.return_value = interactive
mocker.patch("isic_cli.cli.sentry_sdk.is_initialized", return_value=sentry_initialized)

spy = mocker.spy(cli, "capture_exception")
with pytest.raises(SystemExit) as exc_info:
main()

assert exc_info.value.code == 1
assert prompt.called == interactive
assert prompt.called == (interactive and sentry_initialized)
assert spy.call_count == capture_exception_sent
issue_link_shown = (
"https://github.com/ImageMarkup/isic-cli/issues/new" in capsys.readouterr().err
Expand Down
4 changes: 4 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ commands =
[testenv:package]
# PyInstaller needs the real package files in site-packages, not an editable install
package = wheel
passenv =
ISIC_CLI_SENTRY_DSN
dependency_groups =
package
commands =
python scripts/write_sentry_dsn_module.py {env_tmp_dir}/sentry_dsn/_isic_cli_sentry_dsn.py
pyinstaller \
--clean \
--noconfirm \
--onefile \
--name isic \
--paths {env_tmp_dir}/sentry_dsn \
--recursive-copy-metadata isic_cli \
--collect-data isic_cli \
--specpath {env_tmp_dir} \
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading