Coverage for benefits/sentry.py: 81%

57 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-10-21 19:31 +0000

1import logging 

2import shutil 

3import os 

4import subprocess 

5 

6from django.conf import settings 

7import sentry_sdk 

8from sentry_sdk.integrations.django import DjangoIntegration 

9from sentry_sdk.scrubber import EventScrubber, DEFAULT_DENYLIST 

10 

11from benefits import VERSION 

12 

13logger = logging.getLogger(__name__) 

14 

15SENTRY_CSP_REPORT_URI = None 

16 

17 

18def git_available(): 

19 return bool(shutil.which("git")) 

20 

21 

22# https://stackoverflow.com/a/24584384/358804 

23def is_git_directory(path="."): 

24 with open(os.devnull, "w") as dev_null: 

25 return subprocess.call(["git", "-C", path, "status"], stderr=dev_null, stdout=dev_null) == 0 

26 

27 

28# https://stackoverflow.com/a/21901260/358804 

29def get_git_revision_hash(): 

30 return subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("ascii").strip() 

31 

32 

33def get_sha_file_path(): 

34 current_file = os.path.dirname(os.path.abspath(__file__)) 

35 return os.path.join(current_file, "..", "static", "sha.txt") 

36 

37 

38def get_sha_from_file(): 

39 sha_path = get_sha_file_path() 

40 if os.path.isfile(sha_path): 

41 with open(sha_path) as f: 

42 return f.read().strip() 

43 else: 

44 return None 

45 

46 

47def get_release() -> str: 

48 """Returns the first available: the SHA from Git, the value from sha.txt, or the VERSION.""" 

49 

50 if git_available() and is_git_directory(): 

51 return get_git_revision_hash() 

52 else: 

53 sha = get_sha_from_file() 

54 if sha: 

55 return sha 

56 else: 

57 # one of the above *should* always be available, but including this just in case 

58 return VERSION 

59 

60 

61def get_denylist(): 

62 # custom denylist 

63 denylist = DEFAULT_DENYLIST + ["sub", "name"] 

64 return denylist 

65 

66 

67def get_traces_sample_rate(): 

68 try: 

69 rate = float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "0.0")) 

70 if rate < 0.0 or rate > 1.0: 

71 logger.warning("SENTRY_TRACES_SAMPLE_RATE was not in the range [0.0, 1.0], defaulting to 0.0") 

72 rate = 0.0 

73 else: 

74 logger.info(f"SENTRY_TRACES_SAMPLE_RATE set to: {rate}") 

75 except ValueError: 

76 logger.warning("SENTRY_TRACES_SAMPLE_RATE did not parse to float, defaulting to 0.0") 

77 rate = 0.0 

78 

79 return rate 

80 

81 

82def configure(): 

83 sentry_dsn = os.environ.get("SENTRY_DSN") 

84 sentry_environment = os.environ.get("SENTRY_ENVIRONMENT", settings.RUNTIME_ENVIRONMENT()) 

85 

86 if sentry_dsn: 86 ↛ 87line 86 didn't jump to line 87 because the condition on line 86 was never true

87 release = get_release() 

88 logger.info(f"Enabling Sentry for environment '{sentry_environment}', release '{release}'...") 

89 

90 # https://docs.sentry.io/platforms/python/configuration/ 

91 sentry_sdk.init( 

92 dsn=sentry_dsn, 

93 integrations=[ 

94 DjangoIntegration(), 

95 ], 

96 traces_sample_rate=get_traces_sample_rate(), 

97 environment=sentry_environment, 

98 release=release, 

99 in_app_include=["benefits"], 

100 # send_default_pii must be False (the default) for a custom EventScrubber/denylist 

101 # https://docs.sentry.io/platforms/python/data-management/sensitive-data/#event_scrubber 

102 send_default_pii=False, 

103 event_scrubber=EventScrubber(denylist=get_denylist(), recursive=True), 

104 ) 

105 

106 # override the module-level variable when configuration happens, if set 

107 global SENTRY_CSP_REPORT_URI 

108 SENTRY_CSP_REPORT_URI = os.environ.get("SENTRY_REPORT_URI", "") 

109 else: 

110 logger.info("SENTRY_DSN not set, so won't send events")