Coverage for benefits / sentry.py: 80%
55 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-01 15:39 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-01 15:39 +0000
1import datetime
2import os
3import shutil
4import subprocess
6import sentry_sdk
7from sentry_sdk.integrations.django import DjangoIntegration
8from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber
10from benefits import VERSION
12SENTRY_CSP_REPORT_URI = None
15def git_available():
16 return bool(shutil.which("git"))
19# https://stackoverflow.com/a/24584384/358804
20def is_git_directory(path="."):
21 with open(os.devnull, "w") as dev_null:
22 return subprocess.call(["git", "-C", path, "status"], stderr=dev_null, stdout=dev_null) == 0
25# https://stackoverflow.com/a/21901260/358804
26def get_git_revision_hash():
27 return subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("ascii").strip()
30def get_sha_file_path():
31 current_file = os.path.dirname(os.path.abspath(__file__))
32 return os.path.join(current_file, "..", "static", "sha.txt")
35def get_sha_from_file():
36 sha_path = get_sha_file_path()
37 if os.path.isfile(sha_path):
38 with open(sha_path) as f:
39 return f.read().strip()
40 else:
41 return None
44def get_release() -> str:
45 """Returns the first available: the SHA from Git, the value from sha.txt, or the VERSION."""
47 if git_available() and is_git_directory():
48 return get_git_revision_hash()
49 else:
50 sha = get_sha_from_file()
51 if sha:
52 return sha
53 else:
54 # one of the above *should* always be available, but including this just in case
55 return VERSION
58def get_denylist():
59 # custom denylist
60 denylist = DEFAULT_DENYLIST + ["sub", "name"]
61 return denylist
64def get_traces_sample_rate():
65 try:
66 rate = float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "0.0"))
67 if rate < 0.0 or rate > 1.0:
68 print(
69 f"[{datetime.datetime.now().strftime("%d/%b/%Y %H:%M:%S")}] "
70 "WARNING benefits.sentry SENTRY_TRACES_SAMPLE_RATE was not in the range [0.0, 1.0], defaulting to 0.0"
71 )
72 rate = 0.0
73 else:
74 print(
75 f"[{datetime.datetime.now().strftime("%d/%b/%Y %H:%M:%S")}] "
76 f"INFO benefits.sentry SENTRY_TRACES_SAMPLE_RATE set to: {rate}"
77 )
78 except ValueError:
79 print(
80 f"[{datetime.datetime.now().strftime("%d/%b/%Y %H:%M:%S")}] "
81 "WARNING benefits.sentry SENTRY_TRACES_SAMPLE_RATE did not parse to float, defaulting to 0.0"
82 )
83 rate = 0.0
85 return rate
88def configure(runtime_environment):
89 sentry_dsn = os.environ.get("SENTRY_DSN")
90 sentry_environment = os.environ.get("SENTRY_ENVIRONMENT", runtime_environment)
92 if sentry_dsn: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 release = get_release()
94 print(
95 f"[{datetime.datetime.now().strftime("%d/%b/%Y %H:%M:%S")}] "
96 f"INFO benefits.sentry Enabling Sentry for environment '{sentry_environment}', "
97 f"release '{release}'..."
98 )
100 # https://docs.sentry.io/platforms/python/configuration/
101 sentry_sdk.init(
102 dsn=sentry_dsn,
103 integrations=[
104 DjangoIntegration(),
105 ],
106 traces_sample_rate=get_traces_sample_rate(),
107 environment=sentry_environment,
108 release=release,
109 in_app_include=["benefits"],
110 # send_default_pii must be False (the default) for a custom EventScrubber/denylist
111 # https://docs.sentry.io/platforms/python/data-management/sensitive-data/#event_scrubber
112 send_default_pii=False,
113 event_scrubber=EventScrubber(denylist=get_denylist(), recursive=True),
114 )
116 # override the module-level variable when configuration happens, if set
117 global SENTRY_CSP_REPORT_URI
118 SENTRY_CSP_REPORT_URI = os.environ.get("SENTRY_REPORT_URI", "")
119 else:
120 print(
121 f"[{datetime.datetime.now().strftime("%d/%b/%Y %H:%M:%S")}] "
122 "INFO benefits.sentry SENTRY_DSN not set, so won't send events"
123 )