mirror of
https://github.com/audacity/sentry-cleanup-helper.git
synced 2025-12-10 03:56:52 -06:00
Adds a script, that performs Sentry cleanup
This commit is contained in:
commit
50b14d8301
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.venv/
|
||||
.idea/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Audacity Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1
README.md
Normal file
1
README.md
Normal file
@ -0,0 +1 @@
|
||||
A script that cleans up old debug symbols from Audacity Sentry
|
||||
175
main.py
Normal file
175
main.py
Normal file
@ -0,0 +1,175 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
import dateutil.parser
|
||||
import pytz as pytz
|
||||
import requests
|
||||
import re
|
||||
|
||||
dsym_endpoint = "https://sentry.audacityteam.org/api/0/projects/sentry/audacity-crash/files/dsyms/"
|
||||
|
||||
audacity_files = [
|
||||
re.compile(r"Audacity.*"),
|
||||
re.compile(r"audacity.*"),
|
||||
re.compile(r"lib-.+"),
|
||||
re.compile(r"mod-.+"),
|
||||
re.compile(r"crashreporter.*"),
|
||||
re.compile(r"Wrapper.*"),
|
||||
]
|
||||
|
||||
|
||||
def format_file_size(size):
|
||||
if size < 1024:
|
||||
return f'{size} B'
|
||||
elif size < 1024 * 1024:
|
||||
return f'{size / 1024:.2f} KB'
|
||||
elif size < 1024 * 1024 * 1024:
|
||||
return f'{size / 1024 / 1024:.2f} MB'
|
||||
else:
|
||||
return f'{size / 1024 / 1024 / 1024:.2f} GB'
|
||||
|
||||
|
||||
class SentryAuth(requests.auth.AuthBase):
|
||||
def __call__(self, r):
|
||||
r.headers["Authorization"] = f'Bearer {os.environ["SENTRY_TOKEN"]}'
|
||||
return r
|
||||
|
||||
|
||||
class SentryFile:
|
||||
def __init__(self, file_json):
|
||||
self.id = file_json["id"]
|
||||
self.date_created = dateutil.parser.isoparse(file_json['dateCreated'])
|
||||
self.size = int(file_json["size"])
|
||||
self.name = file_json["objectName"]
|
||||
|
||||
|
||||
class Contex:
|
||||
now = datetime.now(tz=pytz.utc)
|
||||
releases = []
|
||||
|
||||
deleted_files_count = 0
|
||||
deleted_files_size = 0
|
||||
|
||||
skipped_release_files_count = 0
|
||||
skipped_release_files_size = 0
|
||||
|
||||
skipped_files_count = 0
|
||||
skipped_files_size = 0
|
||||
|
||||
processed_files_count = 0
|
||||
|
||||
non_audacity_libs = set()
|
||||
|
||||
def __init__(self):
|
||||
self._get_github_releases()
|
||||
self.safe_time = self.now - timedelta(days=7)
|
||||
|
||||
self.session = requests.Session()
|
||||
|
||||
def _get_github_releases(self):
|
||||
current_url = "https://api.github.com/repos/audacity/audacity/releases"
|
||||
|
||||
s = requests.Session()
|
||||
|
||||
while None != current_url:
|
||||
print(f'Requesting {current_url}...')
|
||||
|
||||
r = s.get(current_url)
|
||||
|
||||
for release_json in r.json():
|
||||
self.releases.append(dateutil.parser.isoparse(release_json['published_at']))
|
||||
|
||||
if "next" in r.links:
|
||||
current_url = r.links["next"]["url"]
|
||||
else:
|
||||
current_url = None
|
||||
|
||||
def _is_in_release_timeframe(self, file):
|
||||
delta = timedelta(days=1)
|
||||
for release in self.releases:
|
||||
if release - delta < file.date_created < release:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _is_audacity_file(self, file):
|
||||
for pattern in audacity_files:
|
||||
if pattern.match(file.name):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def process_file(self, file):
|
||||
self.processed_files_count = self.processed_files_count + 1
|
||||
|
||||
skipped = False
|
||||
if self._is_in_release_timeframe(file):
|
||||
skipped = True
|
||||
self.skipped_release_files_count = self.skipped_release_files_count + 1
|
||||
self.skipped_release_files_size = self.skipped_release_files_size + file.size
|
||||
print(
|
||||
f'({self.processed_files_count}) Skipped file {file.name} ({file.id}, {datetime.isoformat(file.date_created)}): matches release.')
|
||||
elif file.date_created > self.safe_time:
|
||||
print(
|
||||
f'({self.processed_files_count}) Skipped file {file.name} ({file.id}, {datetime.isoformat(file.date_created)}): too new.')
|
||||
skipped = True
|
||||
|
||||
is_audacity_file = self._is_audacity_file(file)
|
||||
|
||||
if not skipped and not is_audacity_file:
|
||||
if file.name not in self.non_audacity_libs:
|
||||
print(f'({self.processed_files_count}) Skipped file {file.name} ({file.id}, {datetime.isoformat(file.date_created)}): first occurrence of the file.')
|
||||
skipped = True
|
||||
|
||||
if skipped:
|
||||
if not is_audacity_file:
|
||||
self.non_audacity_libs.add(file.name)
|
||||
|
||||
self.skipped_files_count = self.skipped_files_count + 1
|
||||
self.skipped_files_size = self.skipped_files_size + file.size
|
||||
return
|
||||
|
||||
print(
|
||||
f'({self.processed_files_count}) Deleting file {file.name} ({file.id}, {datetime.isoformat(file.date_created)}).')
|
||||
self.deleted_files_count = self.deleted_files_count + 1
|
||||
self.deleted_files_size = self.deleted_files_size + file.size
|
||||
|
||||
url = f'{dsym_endpoint}?id={file.id}'
|
||||
r = self.session.delete(url, auth=SentryAuth())
|
||||
if r.status_code != 204:
|
||||
print(f'Error deleting file {file.name} ({file.id}, {datetime.isoformat(file.date_created)}): {r.status_code} {r.text}')
|
||||
raise Exception(f'Error deleting file {file.name} ({file.id}, {datetime.isoformat(file.date_created)}): {r.status_code} {r.text}')
|
||||
|
||||
def print_stats(self):
|
||||
print(f'Processed files: {self.processed_files_count}')
|
||||
print(f'Deleted files: {self.deleted_files_count}')
|
||||
print(f'Deleted files size: {format_file_size(self.deleted_files_size)}')
|
||||
print(f'Skipped files: {self.skipped_files_count}')
|
||||
print(f'Skipped files size: {format_file_size(self.skipped_files_size)}')
|
||||
print(f'Skipped release files: {self.skipped_release_files_count}')
|
||||
print(f'Skipped release files size: {format_file_size(self.skipped_release_files_size)}')
|
||||
|
||||
|
||||
def request_dsyms(context):
|
||||
has_more = True
|
||||
current_url = dsym_endpoint
|
||||
|
||||
s = requests.Session()
|
||||
|
||||
while has_more:
|
||||
print(f'Requesting {current_url}...')
|
||||
r = s.get(current_url, auth=SentryAuth())
|
||||
|
||||
for file_json in r.json():
|
||||
file = SentryFile(file_json)
|
||||
context.process_file(file)
|
||||
|
||||
has_more = r.links["next"]["results"] == 'true'
|
||||
current_url = r.links["next"]["url"]
|
||||
|
||||
|
||||
# Press the green button in the gutter to run the script.
|
||||
if __name__ == '__main__':
|
||||
context = Contex()
|
||||
request_dsyms(context)
|
||||
context.print_stats()
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
python-dateutil~=2.8.2
|
||||
pytz~=2022.2.1
|
||||
requests~=2.28.1
|
||||
Loading…
x
Reference in New Issue
Block a user