สวัสดีฉันคือ Vilian Iaumbaev! ฉันเพิ่งสร้างระบบที่จัดการรายงานความผิดพลาดใหม่โดยอัตโนมัติสําหรับ iOS และ Android - มันทําให้การติดตามและแก้ไขปัญหาง่ายขึ้น ทําไมเราทําสิ่งนี้ การกําหนดค่าการล้มเหลวด้วยตนเองให้กับนักพัฒนาที่เหมาะสมได้กลายเป็นงานที่น่าเบื่อและไม่น่าเชื่อถืออย่างรวดเร็ว มันเป็นเรื่องง่ายที่จะลืมบางสิ่งบางอย่างหลีกเลี่ยงกรณีขอบหรือล้มเหลวปัญหาที่ซับซ้อน เราต้องการที่จะทําให้กระบวนการที่คาดการณ์ได้มากขึ้นและเป็นระบบเพื่อให้ไม่มีใครในทีมต้องเสียเวลาในการทดสอบการล้มเหลวและไม่มีปัญหาที่สําคัญจะตกผ่านการล้มเหลว คําอธิบาย ในการเริ่มต้นคุณจะต้องใช้เครื่องมือ Google เช่น , และ Jira Crashlytics แพลตฟอร์ม Google Cloud แครสไลติก แพลตฟอร์ม Google Cloud เมื่อโครงการของคุณได้รับการตั้งค่าในบริการ Google ให้กําหนดค่าการถ่ายโอนข้อมูลจาก Crashlytics ไปยัง GCP โดยใช้ หลังจากนั้นรายงานความผิดพลาดทั้งหมดจะปรากฏในตาราง BigQuery หน้าการรวม หน้าการรวม โครงสร้างข้อมูลการล้มเหลวสําหรับ iOS และ Android เกือบจะเหมือนกันมีเพียงไม่กี่ความแตกต่างเล็ก ๆ น้อย ๆ ซึ่งหมายความว่าเราสามารถใช้สคริปต์เดียวในการประมวลผลทั้งสอง ดังนั้นตอนนี้คุณมีความล้มเหลวของคุณใน BigQuery ซึ่งหมายความว่าคุณสามารถดําเนินการงานบางอย่างเกี่ยวกับข้อมูลนี้ คุณสามารถขอข้อมูลการล้มเหลวทั้งหมดและวิเคราะห์ตามที่คุณต้องการในด้านของคุณ ฉันได้เลือกภาษา Python และจะอธิบายให้คุณในตัวอย่างนี้ ครั้งแรกเราต้องได้รับข้อมูลทั้งหมดที่เกิดความล้มเหลวที่จะได้รับการวิเคราะห์ แต่ถ้าคุณมีปริมาณข้อมูลจํานวนมากเกี่ยวกับผู้ใช้มากกว่าหนึ่งล้านคุณควรประมวลผลข้อมูลทั้งหมดในด้าน Google ทําการรวมกันบางอย่าง แผน เรียนรู้บางขั้นพื้นฐาน SQL เพื่อรับข้อมูลการล้มเหลวจาก BigQuery Query Crash Data โดยใช้ Python รับ committers ทั้งหมดจากสํารองข้อมูลและ merge duplicates แผนที่แต่ละปัญหาเป็นไฟล์ repo และเจ้าของ สร้างงาน Jira สําหรับเจ้าของไฟล์ถ้างานไม่ได้มีอยู่แล้ว เรียนรู้บางส่วนพื้นฐานของ SQL เพื่อรับข้อมูลจาก BigQuery BigQuery ใช้ dialect SQL ของตัวเองซึ่งคล้ายกับ SQL มาตรฐาน แต่ให้ความสะดวกในการวิเคราะห์ข้อมูลเพิ่มเติม สําหรับการบูรณาการของเราเราจําเป็นต้องทํางานกับชุดข้อมูลการล้มเหลวที่สมบูรณ์ แต่ในรูปแบบรวม โดยเฉพาะอย่างยิ่งเราได้กลุ่มรายงานการล้มเหลวแต่ละรายเป็นเอกลักษณ์ลงนามการล้มเหลวแล้วรวมข้อมูลที่เกี่ยวข้องภายในแต่ละกลุ่มเช่นจํานวนการเกิดขึ้นจํานวนผู้ใช้ที่ได้รับผลกระทบการกระจายรุ่นและอื่น ๆ คุณสามารถค้นหาสคริปต์ SQL ด้านล่างและทดสอบในสภาพแวดล้อมของคุณเองผ่านลิงก์ต่อไปนี้: https://console.cloud.google.com/bigquery https://console.cloud.google.com/bigquery 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 เป็นผลให้คุณจะได้รับหนึ่งแถวต่อ issue_id ที่เป็นเอกลักษณ์พร้อมกับฟิลด์รวมต่อไปนี้: issue_titles — รายการของทั้งหมดหัวข้อ crash นี่คือชุดที่จะคํานวณกรณีที่มีหลายหัวข้อที่ไม่ซ้ํากันที่มีอยู่สําหรับปัญหาเดียวกัน ในส่วนสคริปต์เราจะเลือกหนึ่งที่พบบ่อยที่สุด blame_files — รายการของไฟล์ stacktrace ด้านบนที่รับผิดชอบความผิดพลาด นี้จะไม่ว่างเปล่าหากความผิดพลาดเกิดขึ้นในรหัสฐานของคุณ (ไม่ใช่ในห้องสมุดระบบ) blame_libraries — รายการของห้องสมุดที่เกี่ยวข้องกับความล้มเหลว นี่คือนอกจากนี้ยังเป็นมาร์เรย์ที่สร้างขึ้นเพื่อเหตุผลที่คล้ายกับ issue_titles blame_symbols — รายการของสัญลักษณ์รหัส (ฟังก์ชั่น / วิธีการ) ที่เกิดความล้มเหลว เช่นเดียวกับฟิลด์อื่น ๆ ด้านบนมันเป็นมาร์เรย์ total_events – จํานวนเหตุการณ์ที่เกิดขึ้นโดยรวมในช่วงระยะเวลาที่เลือก total_users — จํานวนผู้ใช้ที่ไม่ซ้ํากันที่ได้รับผลกระทบ บางครั้งอาจเกิดความล้มเหลวเฉพาะสําหรับกลุ่มผู้ใช้ที่ระบุ events_info — ตาราง JSON (เป็น string) ที่มี total_events และ total_users ส่วนแบ่งตามรุ่นแอพ ดูตัวอย่างด้านล่าง [ { "version": "1.0.1", "events": 131, "users": 110 }, { "version": "1.2.1", "events": 489, "users": 426 } ] Request crashes ข้อมูลจาก BigQuery โดยใช้ Python เพื่อเริ่มต้นการติดตั้งห้องสมุดลูกค้า BigQuery Python จาก หลังจากติดตั้งสร้างไฟล์ BigQueryExecutor.py - โมดูลนี้จะจัดการกับการสื่อสารทั้งหมดกับ Google Cloud BigQuery จิ๋ม จิ๋ม 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 ชื่อ (หรือ id) ของโครงการ BigQuery ของคุณ เมื่อคุณมีเหล่านี้คุณสามารถตรวจสอบความถูกต้องและเริ่มต้นการเรียกใช้คําถามผ่านสคริปต์ บัญชีบริการ Google JSON credential เพื่อสร้างบัญชีบริการ ไปที่ และให้มัน บทบาท Google Cloud Console BigQuery Data Editor การใช้งานของ Google Cloud Console เมื่อบัญชีถูกสร้างขึ้นเปิดมันนําทางไปยัง แท็บ, คลิก และเลือก นี้จะสร้างและดาวน์โหลดไฟล์การรับรอง JSON สําหรับบัญชีบริการ “Keys” “Add key” “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 เป็น string หนึ่งบรรทัดและบูรณาการโดยตรงในสคริปต์ของคุณ อย่างไรก็ตามวิธีการนี้เป็น — ใช้ผู้จัดการความลับเพื่อจัดเก็บและจัดการข้อมูลส่วนบุคคลของคุณได้อย่างปลอดภัย not recommended for production นอกจากนี้คุณยังสามารถสกัด 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 Hooray! 🎉 ตอนนี้ที่เราสามารถรับข้อมูลการล้มเหลวจาก BigQuery เราสามารถย้ายไปสู่ขั้นตอนต่อไป - เอา Top 5 การล้มเหลวที่พบบ่อยที่สุดและสร้างงาน Jira โดยอัตโนมัติสําหรับพวกเขา รับผู้รับผิดชอบทั้งหมดของบันทึกและรวมพวกเขา ก่อนที่จะกําหนดปัญหาความผิดพลาดให้กับนักพัฒนาเราต้องระบุเจ้าของศักยภาพสําหรับแต่ละความผิดพลาด เพื่อทําเช่นนั้นเราจะเริ่มโดยการรวบรวมผู้เขียนความผิดพลาดทั้งหมดจากสต็อก เนื่องจากเราใช้ GitHub เราควรตระหนักถึงรายละเอียดบางอย่าง: ผู้พัฒนาบางคนอาจใช้ที่อยู่อีเมลหลายที่ผ่านการรับสัญญาดังนั้นเราจึงจําเป็นต้องรวมตัวตนตามที่เหมาะสม GitHub มักใช้อีเมล noreply (เช่น username@users.noreply.github.com) ดังนั้นเราจะจัดการกับกรณีเหล่านี้ตามลําดับ วัตถุประสงค์หลักในขั้นตอนนี้คือการสกัดและปกติรายการผู้เขียน Git กับชื่อและอีเมลของพวกเขาโดยใช้คําสั่งต่อไปนี้: git log grep '^Author' sort 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 (เมื่อมี) สําหรับความสะดวกสบาย ด้วยสคริปต์ด้านล่างคุณสามารถเริ่มต้นกระบวนการนี้และรับรายการที่ทําความสะอาดและ deduplicated ของ committers ทั้งหมดในสต็อก: 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 มักจะแตกต่างกันระหว่าง บริษัท - กรอบการทํางานและสิทธิ์ที่กําหนดเองอาจแตกต่างกัน ฉันขอแนะนําให้อ้างอิงถึงเอกสาร API Jira อย่างเป็นทางการและใช้ลูกค้า Python อย่างเป็นทางการเพื่อให้แน่ใจว่าสามารถเข้ากันได้กับการตั้งค่าของคุณ นี่คือเคล็ดลับการปฏิบัติตามประสบการณ์ของเรา: อย่าสร้างงานสําหรับทุกปัญหา พิจารณาปัญหา 5-10 ด้านบนขึ้นอยู่กับจํานวนผู้ใช้ที่ได้รับผลกระทบหรือขอบเขตผลกระทบบางอย่าง บันทึกข้อมูลเกี่ยวกับงานที่สร้างขึ้นในพื้นที่เก็บข้อมูลที่คงที่ ฉันใช้ BigQuery เพื่อบันทึกข้อมูลในตารางที่แยกต่างหากและอัปเดตข้อมูลในแต่ละสคริปต์ สร้างงานที่ปิดใหม่หากปัญหาเกิดขึ้นอีกครั้งในเวอร์ชันใหม่ ๆ ของแอปนี้เพื่อให้แน่ใจว่าการล้มเหลวจะไม่ถูกลืม การเชื่อมโยงงานสําหรับปัญหาเดียวกันเพื่อให้การตรวจสอบในอนาคตง่ายขึ้นและหลีกเลี่ยงการซ้ํากัน รวมรายละเอียดมากที่สุดเท่าที่จะเป็นไปได้ในคําอธิบายงาน เพิ่มการรวบรวมความล้มเหลวจํานวนผู้ใช้ที่ได้รับผลกระทบเวอร์ชัน ฯลฯ การเชื่อมโยงที่เกี่ยวข้องกับความล้มเหลวถ้าพวกเขากําลังมาจากไฟล์เดียวกัน - นี้ให้ความสัมพันธ์เพิ่มเติม ประกาศทีมของคุณใน Slack (หรือระบบการส่งข้อความอื่น ๆ) เมื่อมีการสร้างงานใหม่หรืองานที่มีอยู่ต้องการความสนใจ รวมลิงก์ที่เป็นประโยชน์ไปยังรายงานความผิดพลาดงานไฟล์ GitHub ที่เกี่ยวข้อง ฯลฯ เพิ่มการจัดการข้อผิดพลาดไปยังสคริปต์ของคุณ ใช้บล็อกทดลอง / ยกเว้นและส่งการแจ้งเตือน Slack เมื่อสิ่งบางอย่างผิดพลาด Cache การดําเนินงานช้าในระหว่างการพัฒนา ตัวอย่างเช่น cache BigQuery crash retrievals ในท้องถิ่นเพื่อเร่ง iteration บางความผิดพลาดอาจเกี่ยวข้องกับห้องสมุดที่ใช้ร่วมกันหรือหลัก ในกรณีเหล่านี้คุณอาจจําเป็นต้องกําหนดภารกิจด้วยตนเอง แต่ยังคงเป็นประโยชน์ในการสร้างปัญหา Jira โดยอัตโนมัติด้วยพื้นฐานความผิดพลาดเต็มรูปแบบ ข้อสรุป ระบบนี้ช่วยให้เราสามารถประมวลผลรายงานความผิดพลาดหลายพันรายวันและนําไปสู่ผู้พัฒนาที่เหมาะสมในไม่กี่นาทีโดยไม่ต้องทํางานด้วยตนเอง หากทีมของคุณจมน้ําในปัญหาการล้มเหลวที่ไม่มีหมวดหมู่ - อัตโนมัติ