
Создание автоматизации отчета об аварии для iOS и Android
8 августа 2025 г.Привет, я Вилиан Яумбэев! Недавно я построил систему, которая автоматически обрабатывает новые отчеты о сбоях как для iOS, так и для Android - это намного облегчает отслеживание и исправление проблем.
Почему мы это сделали.
Вручную назначение сбоев правильным разработчикам быстро стали утомительной и ненадежной задачей. Было легко забыть что -то, упустить из виду корпуса или пропустить сложные проблемы. Мы хотели сделать процесс более предсказуемым и систематическим - так что никто в команде не должен был тратить время на то, чтобы с авариями, и никакие критические проблемы не провалились через трещины.
Краткое содержание
Чтобы начать, вам понадобятся инструменты Google, такие как
Как только ваши проекты будут настроены в службах Google, настройте передачу данных от Crashlytics в GCP, используя
Структуры данных сбоя для iOS и Android практически идентичны, с несколькими небольшими различиями, что означает, что мы можем использовать один сценарий для обработки обоих.
Итак, теперь у вас есть сбои в BigQuery, что означает, что вы можете выполнить некоторую работу по этим данным. Вы можете запросить все данные о сбое и проанализировать их так, как вы хотите на своей стороне.
Я выбрал язык питона и объясню вам в этом примере. Во -первых, нам нужно анализировать все данные о сбоях, но если у вас есть большое количество данных на миллионе пользователей, вам лучше предварительно предварительно обработать все данные на стороне Google, создайте несколько агрегаций.
План
- Узнайте базовый SQL, чтобы получить данные о сбое от BigQuery
- Данные о сбое запросов с помощью Python
- Получите всех коммитников от репозитория и слияния дубликатов
- Карту каждого выпуска в файл репо и его владельца
- Создайте задачу JIRA для владельца файла, если задача уже не существует
Узнайте некоторые основы SQL, чтобы получить данные от BigQuery
BigQuery использует свой собственный диалект SQL, который аналогичен стандартному SQL, но предлагает дополнительное удобство для анализа данных. Для нашей интеграции нам нужно было работать с полным набором данных о сбою, но в агрегированной форме. В частности, мы сгруппировали отдельные отчеты о сбоях в уникальные подписи сбоев, а затем агрегировали соответствующие данные в каждой группе, такие как количество случаев, затронутое количество пользователей, разбивка версии и многое другое. Вы можете найти сценарий SQL ниже и проверить его в своей собственной среде по следующей ссылке:
WITH pre as(
SELECT
issue_id,
ARRAY_AGG(DISTINCT issue_title IGNORE NULLS) as issue_titles,
ARRAY_AGG(DISTINCT blame_frame.file IGNORE NULLS) as blame_files,
ARRAY_AGG(DISTINCT blame_frame.library IGNORE NULLS) as blame_libraries,
ARRAY_AGG(DISTINCT blame_frame.symbol IGNORE NULLS) as blame_symbols,
COUNT(DISTINCT event_id) as total_events,
COUNT(DISTINCT installation_uuid) as total_users,
'{"version":"' || application.display_version || '","events":' || COUNT(DISTINCT event_id)
|| ',"users":' || COUNT(DISTINCT installation_uuid) || '}' AS events_info
FROM `YOUR TABLE NAME`
WHERE 1=1
AND event_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR)
AND event_timestamp < CURRENT_TIMESTAMP()
AND error_type = "FATAL"
GROUP BY issue_id, application.display_version
)
SELECT
issue_id,
ARRAY_CONCAT_AGG(issue_titles) as issue_titles,
ARRAY_CONCAT_AGG(blame_files) as blame_files,
ARRAY_CONCAT_AGG(blame_libraries) as blame_libraries,
ARRAY_CONCAT_AGG(blame_symbols) as blame_symbols,
SUM(total_events) as total_events,
SUM(total_users) as total_users,
'[' || STRING_AGG(events_info, ",") || ']' as events_info
FROM pre
WHERE 1=1
AND issue_id IS NOT NULL
AND events_info IS NOT NULL
GROUP BY issue_id
ORDER BY total_users DESC
В результате вы получите одну строку на уникальный вопрос, а также следующие агрегированные поля:
- angehing_titles- Список всех титулов сбоев. Это массив для учета случаев, когда существует несколько уникальных названий для одной и той же проблемы. В части сценария мы выберем наиболее частую.
- wars_files- Список верхних файлов Stacktrace, обвиненных в сбое. Это будет непустым, если сбой произошел в вашей кодовой базе (а не в системных библиотеках).
- wars_libraries- Список библиотек, связанных с аварией. Это также массив, построенный по причинам, аналогичными выпуска_titles.
- flans_symbols- Список символов кода (функции/методы), где произошел сбой. Как и другие поля выше, это массив.
- total_events- Общее количество случаев авакуляции в течение выбранного периода времени.
- total_users- Количество уникальных пользователей затронуло. Иногда сбой может происходить только для определенной группы пользователей.
- Events_info- массив JSON (в качестве строки), содержащий Total_events и Total_users, разбитые версией приложения. См. Пример ниже.
[
{ "version": "1.0.1", "events": 131, "users": 110 },
{ "version": "1.2.1", "events": 489, "users": 426 }
]
Запрос сбивает данные из BigQuery с помощью Python
Чтобы начать, установите библиотеку BigQuery Python Client от
import os
import json
import tempfile
from google.oauth2 import service_account
from google.cloud import bigquery
from collections import Counter
class BigQueryExecutor:
def __init__(self, credentialsJson: str, bqProjectId: str = ''):
temp_file_path=''
with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file:
json.dump(json.loads(credentialsJson), temp_file, indent=4)
temp_file_path = temp_file.name
credentials = service_account.Credentials.from_service_account_file(temp_file_path)
os.remove(temp_file_path)
self.client = bigquery.Client(project=bqProjectId, credentials=credentials)
Чтобы начать использовать сценарий, вам понадобятся всего две вещи:
- Файл учетной записи службы Google JSON
- Название (или идентификатор) вашего проекта BigQuery
Как только у вас есть их, вы можете аутентифицировать и начать выполнять запросы через сценарий.
Учетная запись сервиса Google JSON
Чтобы создать учетную запись службы, перейдите в
Как только учетная запись создана, откройте его, перейдите к«Ключи»Вкладка, нажмите«Добавить ключ»и выберите«Json»Полем Это будет генерировать и загрузить файл учетных данных JSON для учетной записи службы.
Сервисная учетная запись json обычно выглядит так:
{
"type": "service_account",
"project_id": YOUR_PROJECT,
"private_key_id": private_key_id,
"private_key": GCP_PRIVATE_KEY,
"client_email": "email",
"client_id": "id",
"auth_uri": "auth_uri",
"token_uri": "token_uri",
"auth_provider_x509_cert_url": "auth_provider_x509_cert_url",
"client_x509_cert_url": "url",
"universe_domain": "universe_domain"
}
В целях тестирования вы можете преобразовать учетные данные JSON в однострочную строку и встроить ее непосредственно в ваш сценарий. Однако этот подходне рекомендуется для производства- Используйте менеджер Secrets, чтобы надежно хранить и управлять своими учетными данными.
Вы также можете извлечь свой BQProjectId из поля Project_id в учетные данные JSON.
Модели
Чтобы работать с данными BigQuery в зависимости от типа, полезно определить модели данных, которые отражают структуру результатов запроса. Это позволяет вам писать более чистый, более безопасный и более поддерживаемый код.
Ниже приведен пример таких модельных классов:
class BQCrashlyticsVersionsModel:
def __init__(self,
version: str,
events: int,
users: int
):
self.version = version
self.events = events
self.users = users
class BQCrashlyticsIssueModel:
def __init__(self,
issue_id: str,
issue_title: str,
blame_file: str,
blame_library: str,
blame_symbol: str,
total_events: int,
total_users: int,
versions: list[BQCrashlyticsVersionsModel]
):
self.issue_id = issue_id
self.issue_title = issue_title
self.blame_file = blame_file
self.blame_library = blame_library
self.blame_symbol = blame_symbol
self.total_events = total_events
self.total_users = total_users
self.versions = versions
getCrashlyticsissues функции
И, наконец, мы можем получить данные из BigQuery.
Добавьте следующий метод в ваш существующий класс BigQueryExecutor - он выполнит запрос SQL, описанный ранее вBigQuery SQLраздел и верните результаты, проповедованные в модельные экземпляры.
def getCrashlyticsIssues(self, lastHoursCount: int, tableName: str) -> list[BQCrashlyticsIssueModel]:
firstEventsInfo = """'[' || STRING_AGG(events_info, ",") || ']' as events_info"""
asVersions = """
'{"version":"' || application.display_version || '","events":' || COUNT(DISTINCT event_id)
|| ',"users":' || COUNT(DISTINCT installation_uuid) || '}' AS events_info
"""
query = f"""
WITH pre as(
SELECT
issue_id,
ARRAY_AGG(DISTINCT issue_title IGNORE NULLS) as issue_titles,
ARRAY_AGG(DISTINCT blame_frame.file IGNORE NULLS) as blame_files,
ARRAY_AGG(DISTINCT blame_frame.library IGNORE NULLS) as blame_libraries,
ARRAY_AGG(DISTINCT blame_frame.symbol IGNORE NULLS) as blame_symbols,
COUNT(DISTINCT event_id) as total_events,
COUNT(DISTINCT installation_uuid) as total_users,
{asVersions}
FROM `{tableName}`
WHERE 1=1
AND event_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL {lastHoursCount} HOUR)
AND event_timestamp < CURRENT_TIMESTAMP()
AND error_type = "FATAL"
GROUP BY issue_id, application.display_version
)
SELECT
issue_id,
ARRAY_CONCAT_AGG(issue_titles) as issue_titles,
ARRAY_CONCAT_AGG(blame_files) as blame_files,
ARRAY_CONCAT_AGG(blame_libraries) as blame_libraries,
ARRAY_CONCAT_AGG(blame_symbols) as blame_symbols,
SUM(total_events) as total_events,
SUM(total_users) as total_users,
{firstEventsInfo}
FROM pre
WHERE 1=1
AND issue_id IS NOT NULL
AND events_info IS NOT NULL
GROUP BY issue_id
ORDER BY total_users DESC
"""
bqRows = self.client.query(query).result()
rows: list[BQCrashlyticsIssueModel] = []
def mergeArray(array: list[str]) -> str:
if not array:
return ''
counter = Counter(array)
most_common = counter.most_common(1)
return most_common[0][0] if most_common else ''
for row in bqRows:
issueModel = BQCrashlyticsIssueModel(
issue_id=row.issue_id,
issue_title=mergeArray(row.issue_titles),
blame_file=mergeArray(row.blame_files),
blame_library=mergeArray(row.blame_libraries),
blame_symbol=mergeArray(row.blame_symbols),
total_events=row.total_events,
total_users=row.total_users,
versions=[BQCrashlyticsVersionsModel(version=jj['version'], events=jj['events'], users=jj['users']) for jj in json.loads(row.events_info)]
)
rows.append(issueModel)
return rows
Теперь мы можем выполнить наш запрос SQL в BigQuery непосредственно из Python.
Вот полный пример того, как запустить запрос и работать с результатами:
executor = BigQueryExecutor(credentialsJson=_jsonToken, bqProjectId=_bqProjectId)
allCrashes = executor.getCrashlyticsIssues(lastHoursCount=24, tableName="tableNAME_IOS_REALTIME")
for i in allCrashes:
print(i.issue_title)
[libswift_Concurrency.dylib] swift::swift_Concurrency_fatalError(unsigned int, char const*, ...)
[SwiftUI] static DisplayList.ViewUpdater.Platform.updateClipShapesAsync(layer:oldState:newState:)
Foundation
[SwiftUI] __swift_memcpy112_8
[libswift_Concurrency.dylib] swift::AsyncTask::waitFuture(swift::AsyncTask*, swift::AsyncContext*, void (swift::AsyncContext* swift_async_context) swiftasynccall*, swift::AsyncContext*, swift::OpaqueValue*)
[libobjc.A.dylib] objc_opt_respondsToSelector
[VectorKit] void md::COverlayRenderLayer::layoutRibbon<md::Ribbons::PolylineOverlayRibbonDescriptor>(std::__1::unique_ptr<md::PolylineOverlayLayer<md::Ribbons::PolylineOverlayRibbonDescriptor>, std::__1::default_delete<md::PolylineOverlayLayer<md::Ribbons::PolylineOverlayRibbonDescriptor> > > const&, ggl::CommandBuffer*, md::PolylineOverlayLayoutContext&, unsigned int, unsigned long long, bool, bool, float)
[libswiftCore.dylib] _swift_release_dealloc
[libobjc.A.dylib] objc_msgSend
Ура! 🎉 Теперь, когда мы можем получить данные о сбоях из BigQuery, мы можем перейти к следующему шагу - принять 5 самых частых сбоев и автоматически создавать для них задачи JIRA.
Получите все претенденты на хранилище и объедините их
Прежде чем назначить проблемы с аварией разработчикам, нам сначала необходимо определить потенциальных владельцев для каждого аварии. Для этого мы начнем с сбора всех авторов от репозитория.
Поскольку мы используем GitHub, мы должны знать о нескольких конкретных деталях:
- Некоторые разработчики могут использовать несколько адресов электронной почты в разных коммитах, поэтому нам нужно объединить личности, где это применимо.
- Github часто использует электронные письма (например, username@users.noreply.github.com), поэтому мы будем рассматривать эти случаи соответственно.
Основная цель на этом этапе - извлечь и нормализовать список авторов GIT с их именами и электронными письмами, используя следующую команду:
git log | grep ‘^Автор '| Сорт | Uniq -c
import re
class GitUserModel:
def __init__(self,
nicknames: set[str],
emails: set[str],
gitLogins: set[str]
):
self.nicknames = nicknames
self.emails = emails
self.gitLogins = gitLogins
def returnPossibleNicknames(text: str) -> set[str]:
res = [findEmail(text), loginFromEmail(text), findGitNoreplyLogin(text)]
return set(list(filter(None, res)))
def findEmail(text: str) -> str:
e = re.match(r"(([A-Za-z0-9+\.\_\-]*@[A-Za-z0-9+]*\.[A-Za-z0-9+]*))", text)
if e:
return e.group(1)
def loginFromEmail(text: str) -> str:
e = re.match(r"(([A-Za-z0-9+\.\_\-]*))@[A-Za-z0-9+]*\.[A-Za-z0-9+]*", text)
if e:
return e.group(1)
def findGitNoreplyLogin(text: str) -> str:
gu = re.match(r"\d+\+(([A-Za-z0-9+\.\_\-]*))@users\.noreply\.github\.com", text)
if gu:
return gu.group(1)
else:
gu = re.match(r"(([A-Za-z0-9+\.\_\-]*))@users\.noreply\.github\.com", text)
if gu:
return gu.group(1)
class GitBlamer:
def getAllRepoUsersMap(self, projectRootPath: str) -> list[GitUserModel]:
users: list[GitUserModel] = []
allGitLog = os.popen("cd {}; git log | grep '^Author' | sort | uniq -c".format(projectRootPath)).read()
for line in allGitLog.split('\n'):
user = self._createUserFromBlameLine(line)
if user:
users.append(user)
self._enrichUsersNicknames(users=users)
users = self._mergeSameUsers(users)
users = sorted(users, key=lambda x: list(x.emails)[0] if x.emails else list(x.gitLogins)[0] if x.gitLogins else "")
return users
def _createUserFromBlameLine(self, line):
m = re.match(r".* Author: (.*) <(.*)>", line)
user = GitUserModel(nicknames=set(), emails=set(), gitLogins=set())
if m:
val=set()
if m.group(1): val.add(m.group(1))
if m.group(2): val.add(m.group(2))
user.nicknames = val
else:
return
return user
def _enrichUsersNicknames(self, users: list[GitUserModel]):
for user in users:
possibleNicknames = set()
for nick in user.nicknames:
possibleNicknames = possibleNicknames.union(returnPossibleNicknames(text=nick))
e = findEmail(text=nick)
if e:
user.emails.add(e)
gu = findGitNoreplyLogin(text=nick)
if gu:
user.gitLogins.add(gu)
user.nicknames = user.nicknames.union(possibleNicknames)
def _mergeSameUsers(self, users: list[GitUserModel]):
for i in range(0, len(users)):
if i >= len(users): break
for j in range(i+1, len(users)):
if j >= len(users): break
setLoweredJNicknames=set([u.lower() for u in users[j].nicknames])
for k in range(0, j):
if k >= j: break
setLoweredKNicknames=set([u.lower() for u in users[k].nicknames])
isSameNickname=len(setLoweredKNicknames.intersection(setLoweredJNicknames)) > 0
if isSameNickname:
users[j].gitLogins = users[j].gitLogins.union(users[k].gitLogins)
users[j].emails = users[j].emails.union(users[k].emails)
users.pop(k)
break
return users
В приведенном ниже коде мы пытаемся сопоставить различные личности коммита, которые, вероятно, принадлежат одному и тому же человеку - например, user@gmail.com и user@users.noreply.github.com. Мы также извлекаем и группируем их имена и имена пользователей GitHub (где это возможно) для удобства.
С помощью сценария ниже вы можете запустить этот процесс и получить очищенный, дедуплицированный список всех коммитников в репозитории:
projectRootPath="/IOS_project_path"
blamer = GitBlamer()
allUsers = blamer.getAllRepoUsersMap(projectRootPath=projectRootPath)
for user in allUsers:
print(", ".join(user.nicknames))
Карту каждого выпуска в файл репозитория и владельца файла
На этом этапе у нас есть подробная информация о наших сбоях и пользователях, затронутых ими. Это позволяет нам связывать конкретный сбой с конкретным пользователем и автоматически создавать соответствующую задачу JIRA.
Прежде чем внедрить логику отображения сбоев к пользователю, мы отделили рабочие процессы для iOS и Android. Эти платформы используют разные форматы символов, и критерии для связывания файлов сбоев с проблемами также различаются. Чтобы справиться с этим, мы представили абстрактный класс с реализациями, специфичными для платформы, позволяя нам инкапсулировать различия и решать проблему структурированным образом.
class AbstractFileToIssueMapper:
def isPathBelongsToIssue(self, file: str, filePath: str, issue: BQCrashlyticsIssueModel) -> bool:
raise Exception('Not implemented method AbstractFileToIssueMapper')
class AndroidFileToIssueMapper(AbstractFileToIssueMapper):
def __init__(self):
self.libraryName = 'inDrive'
def isPathBelongsToIssue(self, file: str, filePath: str, issue: BQCrashlyticsIssueModel) -> bool:
if file != issue.blame_file or not issue.blame_symbol.startswith(self.libraryName):
return False
fileNameNoExtension = file.split('.')[0]
fileNameIndexInSymbol = issue.blame_symbol.find(fileNameNoExtension)
if fileNameIndexInSymbol < 0:
return False
relativeFilePathFromSymbol = issue.blame_symbol[0:fileNameIndexInSymbol].replace('.', '/')
relativeFilePathFromSymbol = relativeFilePathFromSymbol + file
return filePath.endswith(relativeFilePathFromSymbol)
class IosFileToIssueMapper(AbstractFileToIssueMapper):
def __init__(self):
self.indriveLibraryName = 'inDrive'
self.indriveFolderName = 'inDrive'
self.modulesFolderName = 'Modules'
def isPathBelongsToIssue(self, file: str, filePath: str, issue: BQCrashlyticsIssueModel) -> bool:
if file != issue.blame_file:
return False
isMatchFolder = False
if issue.blame_library == self.indriveLibraryName:
isMatchFolder = filePath.startswith('{}/'.format(self.indriveFolderName))
else:
isMatchFolder = filePath.startswith('{}/{}/'.format(self.modulesFolderName, issue.blame_library))
return isMatchFolder
Конкретная реализация может варьироваться в зависимости от вашего проекта, но основная ответственность этого класса состоит в том, чтобы определить, произошла ли данная сбоя в определенном файле.
Как только эта логика будет на месте, мы можем перейти к карту файлов по вопросам и назначить их соответствующим владельцам файлов.
import subprocess
class MappedIssueFileModel:
def __init__(self,
fileGitLink: str,
filePath: str,
issue: BQCrashlyticsIssueModel,
fileOwner: GitUserModel
):
self.fileGitLink = fileGitLink
self.filePath = filePath
self.issue = issue
self.fileOwner = fileOwner
class BigQueryCrashesFilesMapper:
def getBlameOut(self, filePath: str, projectRootPath: str) -> list[str]:
dangerousChars = re.compile(r'[;|&\r\n]|\.\.')
if dangerousChars.search(filePath) or dangerousChars.search(projectRootPath):
return None
if not subprocess.check_output(['git', 'ls-files', filePath], cwd=projectRootPath, text=True):
return None
blameProc = subprocess.Popen(['git', 'blame', filePath, '-cwe'], cwd=projectRootPath, stdout=subprocess.PIPE, text=True)
blameRegex=r'<[a-zA-Z0-9\+\.\_\-]*@[a-zA-Z0-9\+\.\_\-]*>'
grepProc = subprocess.Popen(['grep', '-o', blameRegex], stdin=blameProc.stdout, stdout=subprocess.PIPE, text=True)
blameProc.stdout.close()
sortProc = subprocess.Popen(['sort'], stdin=grepProc.stdout, stdout=subprocess.PIPE, text=True)
grepProc.stdout.close()
uniqProc = subprocess.Popen(['uniq', '-c'], stdin=sortProc.stdout, stdout=subprocess.PIPE, text=True)
sortProc.stdout.close()
finalProc = subprocess.Popen(['sort', '-bgr'], stdin=uniqProc.stdout, stdout=subprocess.PIPE, text=True)
uniqProc.stdout.close()
blameOut, _ = finalProc.communicate()
blameArray=list(filter(len, blameOut.split('\n')))
return blameArray
def findFileOwner(self, filePath: str, gitFileOwners: list[GitUserModel], projectRootPath: str) -> GitUserModel:
blameArray = self.getBlameOut(filePath=filePath, projectRootPath=projectRootPath)
if not blameArray:
return
foundAuthor = None
for blameI in range(0, len(blameArray)):
author = re.match(r".*\d+ <(.*)>", blameArray[blameI])
if author:
possibleNicknames = returnPossibleNicknames(text=author.group(1))
for gitFileOwner in gitFileOwners:
if len(gitFileOwner.nicknames.intersection(possibleNicknames)) > 0:
foundAuthor = gitFileOwner
break
if foundAuthor:
break
return foundAuthor
def mapBQResultsWithFiles(self,
fileToIssueMapper: AbstractFileToIssueMapper,
issues: list[BQCrashlyticsIssueModel],
gitFileOwners: list[GitUserModel],
projectRootPath: str
) -> list[MappedIssueFileModel]:
mappedArray: list[MappedIssueFileModel] = []
githubMainBranch = "https://github.com/inDriver/UDF/blob/master"
for root, dirs, files in os.walk(projectRootPath):
for file in files:
filePath = os.path.join(root, file).removeprefix(projectRootPath).strip('/')
gitFileOwner = None
for issue in issues:
if fileToIssueMapper.isPathBelongsToIssue(file, filePath, issue):
if not gitFileOwner:
gitFileOwner = self.findFileOwner(filePath=filePath, gitFileOwners=gitFileOwners, projectRootPath=projectRootPath)
mappedIssue = MappedIssueFileModel(
fileGitLink='{}/{}'.format(githubMainBranch, filePath.strip('/')),
filePath=filePath,
issue=issue,
fileOwner=gitFileOwner
)
mappedArray.append(mappedIssue)
mappedArray.sort(key=lambda x: x.issue.total_users, reverse=True)
return mappedArray
Все, что вам нужно сделать на этом этапе, это обновить свойство GitHubMainBranch по ссылке на ваш собственный репозиторий.
Затем мы собираем проблемы и владельцы файлов, соответственно отображаются файлы, используя приведенный ниже код, и получаем окончательный результат - список проблем, отсортированных по Total_users в порядке убывания.
mapper = BigQueryCrashesFilesMapper()
mappedIssues = mapper.mapBQResultsWithFiles(
fileToIssueMapper=IosFileToIssueMapper(),
issues=allCrashes,
gitFileOwners=allUsers,
projectRootPath=projectRootPath
)
for issue in mappedIssues:
if issue.fileOwner:
print(list(issue.fileOwner.nicknames)[0], issue.issue.total_users, issue.issue.issue_title)
else:
print('no owner', issue.issue.total_users, issue.issue.issue_title)
Создайте задачу JIRA для владельца файла сбоя
На данный момент у нас есть все, что нам нужно, чтобы начать создавать задачи JIRA для аварийных владельцев. Тем не менее, имейте в виду, что конфигурации JIRA часто варьируются между компаниями - пользовательские области, рабочие процессы и разрешения могут отличаться. Я рекомендую ссылаться на официальную документацию JIRA API и использовать их официальный клиент Python для обеспечения совместимости с вашей настройкой.
Вот несколько практических советов, основанных на нашем опыте:
- Не создавайте задачи для каждой проблемы.Сосредоточьтесь на 5–10 проблемах, основанных на количестве затронутых пользователей или определенного порога воздействия.
- Постоянные метаданные задачи.Храните информацию о созданных задачах в постоянном хранилище. Я использую BigQuery, сохраняю данные в отдельной таблице и обновляю их в каждом запуска сценария.
- Воссоздать закрытые задачи, если проблема появитсяВ новых версиях приложения - это гарантирует, что регрессии не игнорируются.
- Ссылка задач по той же проблемеЧтобы упростить будущее расследование и избежать дублирования.
- Включите как можно больше деталей в описание задачи.Добавить агрегации сбоя, затронутые количество пользователей, версии и т. Д.
- Связанные связанные сбоиЕсли они происходят из одного и того же файла - это обеспечивает дополнительный контекст.
- Уведомить свою командуВ Slack (или другой системе обмена сообщениями), когда создаются новые задачи или существующие, требуют внимания. Включите полезные ссылки на отчет о сбое, задачу, соответствующие файлы GitHub и т. Д.
- Добавьте обработку ошибок в свой сценарий.Используйте Try/кроме блоков и отправьте предупреждения Slack, когда что -то не удается.
- Кэш медленные операции во время разработки.Например, кэш -бигкери -избавление локально для ускорения итерации.
- Некоторые сбои могут включать общие или основные библиотеки.В этих случаях вам, вероятно, нужно будет вручную назначить задачу, но все равно полезно создать проблему Jira автоматически с полным контекстом сбоя.
Заключение
Эта система позволяет нам ежедневно обрабатывать тысячи сообщений о сбоях и направлять их к нужному разработчику всего за несколько минут - без каких -либо ручной работы.
Если ваша команда тонет в вопросах с авариями без категории - автоматизируйте ее. 🙌
НаписаноВилиан ЯумбПолем
Оригинал