Crashlytics|Big Query 串接教学:自动查询并转发 iOS App 闪退报告到 Slack
学习如何利用 Crashlytics 与 Big Query 自动查询近 7 天 iOS App 闪退问题,并透过 Google Apps Script 定时转发到 Slack,减少手动追踪时间,提升团队即时掌握闪退状况与修复效率。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
Crashlytics + Big Query 打造更即时便利的 Crash 追踪工具
串接 Crashlytics 和 Big Query 自动转发闪退记录到 Slack Channel
成果
Pinkoi iOS Team 实拍图
先上成果图,每周定时查询 Crashlytics 闪退纪录;筛选出闪退次数前 10 多的问题;将讯息发送到 Slack Channel,方便所有 iOS 队友快速了解目前稳定性。
问题
于 App 开发者来说 Crash-Free Rate 可以说是最重要的衡量指标;数据代表的意思是 App 的使用者 没遇到 闪退的比例,我想不管是什么 App 都应该希望自己的 Crash-Free Rate ~= 99.9%;但现实是不可能的,只要是程式就可能会有 Bug 更何况有的闪退问题是底层(Apple)或第三方 SDK 造成的,另外随著 DAU 体量不同,也会对 Crash-Free Rate 有一定影响,DAU 越高越容易踩到很多偶发的闪退问题。
既然 100% 不会闪退的 App 并不存在,那如何追踪、处理闪退就是一件很重要的事;除了最常见的 Google Firebase Crashlytics (前生 Fabric) 外其实还有其他选择 Bugsnag 、 Bugfender …各工具我没有实际比较过,有兴趣的朋友可以自行研究;如果是用其他工具就用不到本篇文章要介绍的内容了。
Crashlytics
选择使用 Crashlytics 有以下好处:
稳定,由 Google 撑腰
免费、安装便利快速
除闪退外,也可 Log Error Event (EX: Decode Error)
一套 Firebase 即可打天下:其他服务还有 Google Analytics、Realtime Database、Remote Config、Authentication、Cloud Messaging、Cloud Storage…
题外话:不建议正式的服务完全使用 Firebase 搭建,因为后期流量起来后的收费会很贵…就是个养套杀的概念。
Crashlytics 缺点也很多:
Crashlytics 不提供 API 查询闪退资料
Crashlytics 仅会储存近 90 天闪退纪录
Crashlytics 的 Integrations 支援跟弹性极差
最痛的就是 Integrations 支援跟弹性极差再加上又没有 API 可以自己写脚本串闪退资料;只能三不五时靠人工手动上 Crashlytics 查看闪退纪录,追踪闪退问题。
Crashlytics 只支援的 Integrations:
[Email 通知] — Trending stability issues (越来越多人遇到的闪退问题)
[Slack, Email 通知] — New Fatal Issue (闪退问题)
[Slack, Email 通知] — New Non-Fatal Issue (非闪退问题)
[Slack, Email 通知] — Velocity Alert (数量突然一直上升的闪退问题)
[Slack, Email 通知] — Regression Alert (已 Solved 但又出现的问题)
Crashlytics to Jira issue
以上 Integrations 的内容、规则都无法客制化。
最一开始我们直接使用 2.New Fatal Issue to Slack or Email,to Email 的话再由 Google Apps Script 触发后续处理脚本 ;但是这个通知会疯狂轰炸通知频道,因为不管是大是小或只是使用者装置、iOS 本身很零星的问题造成的闪退都会通知;随著 DAU 增长每天都被这通知狂轰滥炸,而其中真的有价值,很多人踩到而且是跟我们程式错误有关的通知大概只占其中的 10%。
以至于根本没有解决 Crashlytics 难以自动追踪的问题,一样要花很多时间在审阅这个问题究竟重不重要之上。
Crashlytics + Big Query
转来转去只找到这个方法,官方也只提供这个方法;这就是免费糖衣下的陷阱,我猜不管是 Crashlytics 或 Analytics Event 都不会也没有计划推出 API 让使用者可以串 API 查资料;因为官方的唯一建议就是把资料汇入到 Big Query 使用,而 Big Query 超过免费储存与查询额度是要收费的。
储存:每个月前 10 GB 为免费。
查询:每个月前 1 TB 为免费。 (查询额度的意思是下 Select 时处理了多少容量的资料)
详细可参考 Big Query 定价说明
Crashlytics to Big Query 的设定细节可参考 官方文件 ,需启用 GCP 服务、绑定信用卡…等等。
开始使用 Big Query 查询 Crashlytics Log
设好 Crashlytics Log to Big Query 汇入周期&完成第一次汇入有资料后,我们就能开始查询资料啰。
首先到 Firebase 专案 -> Crashlytics -> 列表右上方的「•••」-> 点击前往「BigQuery dataset」。
前往 GCP -> Big Query 后可在左方「Exploer」中选择「firebase_crashlytics」->选择你的 Table 名称 ->「Detail」 -> 右边可查看 Table 资讯,包含最新修改时间、已使用容量、储存期限…等等。
确认已有汇入的资料可查询。
上方 Tab 可切换到「SCHEMA」查看 Table 的栏位资讯或参考 官方文件 。
点击右上方的「Query」可开启带有辅助 SQL Builder 的介面(如对 SQL 不熟建议使用这个):
或直接点「COMPOSE NEW QUERY」开一个空白的 Query Editor:
不管是哪种方法,都是同个文字编辑器;在输入完 SQL 之后可以预先在右上方自动完成 SQL 语法检查和预计会花费的查询额度( This query will process XXX when run.
):
确认要查询后点左上方「RUN」执行查询,结果会在下方 Query results 区块显示。
⚠️ 按下「RUN」执行查询后就会累积到查询额度,然后进行收费;所以请注意不要乱下 Query。
如对 SQL 较陌生可以先了解基本用法,然后参考 Crashlytics 官方的范例下去魔改 :
1.统计近 30 日每天的闪退次数:
1
2
3
4
5
6
7
8
9
10
SELECT
COUNT(DISTINCT event_id) AS number_of_crashes,
FORMAT_TIMESTAMP("%F", event_timestamp) AS date_of_crashes
FROM
`你的ProjectID.firebase_crashlytics.你的TableName`
GROUP BY
date_of_crashes
ORDER BY
date_of_crashes DESC
LIMIT 30;
2.查询近 7 天最常出现的 TOP 10 闪退:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SELECT
DISTINCT issue_id,
COUNT(DISTINCT event_id) AS number_of_crashes,
COUNT(DISTINCT installation_uuid) AS number_of_impacted_user,
blame_frame.file,
blame_frame.line
FROM
`你的ProjectID.firebase_crashlytics.你的TableName`
WHERE
event_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(),INTERVAL 168 HOUR)
AND event_timestamp < CURRENT_TIMESTAMP()
GROUP BY
issue_id,
blame_frame.file,
blame_frame.line
ORDER BY
number_of_crashes DESC
LIMIT 10;
但官方范例这个下法查出来的资料跟 Crashlytics 看到的排序不一样,应该是它用 blame_frame.file (nullable), blame_frame.line (nullable) 去 Group 的原因导致。
3.查询近 7 天最常闪退的 10 种装置:
1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT
device.model,
COUNT(DISTINCT event_id) AS number_of_crashes
FROM
`你的ProjectID.firebase_crashlytics.你的TableName`
WHERE
event_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 168 HOUR)
AND event_timestamp < CURRENT_TIMESTAMP()
GROUP BY
device.model
ORDER BY
number_of_crashes DESC
LIMIT 10;
更多范例请参考 官方文件 。
如果你下的 SQL 无任何资料,请先确定指定条件的 Crashlytics 资料已汇入 Big Query(例如预设的 SQL 范例会查当天 Crash 纪录,但其实资料还没同步汇入进来,所以会查不到);如果确定有资料,再来检查筛选条件是否正确。
Top 10 Crashlytics Issue Big Query SQL
这边参考第 2. 的官方范例修改,我们希望的结果是跟我们看 Crashlytics 第一页时一样的闪退问题及排序资料。
近 7 日闪退问题的 Top 10:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SELECT
DISTINCT issue_id,
issue_title,
issue_subtitle,
COUNT(DISTINCT event_id) AS number_of_crashes,
COUNT(DISTINCT installation_uuid) AS number_of_impacted_user
FROM
`你的ProjectID.firebase_crashlytics.你的TableName`
WHERE
is_fatal = true
AND event_timestamp >= TIMESTAMP_SUB(
CURRENT_TIMESTAMP(),
INTERVAL 7 DAY
)
GROUP BY
issue_id,
issue_title,
issue_subtitle
ORDER BY
number_of_crashes DESC
LIMIT
10;
比对 Crashlytics 的 Top 10 闪退问题结果,符合✅。
使用 Google Apps Script 定期查询&转发到 Slack
前往 Google Apps Script 首页 -> 登入与 Big Query 同个帐户 -> 点左上角「新专案」,开启新专案后可点左上方重新命名专案。
首先我们先完成串接 Big Query 取得查询资料:
参考 官方文件 范例,将上面的 Query SQL 带入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function queryiOSTop10Crashes() {
var request = {
query: 'SELECT DISTINCT issue_id, issue_title, issue_subtitle, COUNT(DISTINCT event_id) AS number_of_crashes, COUNT(DISTINCT installation_uuid) AS number_of_impacted_user FROM `firebase_crashlytics.你的TableName` WHERE is_fatal = true AND event_timestamp >= TIMESTAMP_SUB( CURRENT_TIMESTAMP(), INTERVAL 7 DAY ) GROUP BY issue_id, issue_title, issue_subtitle ORDER BY number_of_crashes DESC LIMIT 10;',
useLegacySql: false
};
var queryResults = BigQuery.Jobs.query(request, '你的ProjectID');
var jobId = queryResults.jobReference.jobId;
// Check on status of the Query Job.
var sleepTimeMs = 500;
while (!queryResults.jobComplete) {
Utilities.sleep(sleepTimeMs);
sleepTimeMs *= 2;
queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId);
}
// Get all the rows of results.
var rows = queryResults.rows;
while (queryResults.pageToken) {
queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId, {
pageToken: queryResults.pageToken
});
Logger.log(queryResults.rows);
rows = rows.concat(queryResults.rows);
}
var data = new Array(rows.length);
for (var i = 0; i < rows.length; i++) {
var cols = rows[i].f;
data[i] = new Array(cols.length);
for (var j = 0; j < cols.length; j++) {
data[i][j] = cols[j].v;
}
}
return data
}
query: 餐数可任意更换成写好的 Query SQL。
回传的物件结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
[
[
"67583e77da3b9b9d3bd8feffeb13c8d0",
"<compiler-generated> line 2147483647",
"specialized @nonobjc NSAttributedString.init(data:options:documentAttributes:)",
"417",
"355"
],
[
"a590d76bc71fd2f88132845af5455c12",
"libnetwork.dylib",
"nw_endpoint_flow_copy_path",
"259",
"207"
],
[
"d7c3b750c3e5587c91119c72f9f6514d",
"libnetwork.dylib",
"nw_endpoint_flow_copy_path",
"138",
"118"
],
[
"5bab14b8f8b88c296354cd2e",
"CoreFoundation",
"-[NSCache init]",
"131",
"117"
],
[
"c6ce52f4771294f9abaefe5c596b3433",
"XXX.m line 975",
"-[XXXX scrollToMessageBottom]",
"85",
"57"
],
[
"712765cb58d97d253ec9cc3f4b579fe1",
"<compiler-generated> line 2147483647",
"XXXXX.heightForRow(at:tableViewWidth:)",
"67",
"66"
],
[
"3ccd93daaefe80f024cc8a7d0dc20f76",
"<compiler-generated> line 2147483647",
"XXXX.tableView(_:cellForRowAt:)",
"59",
"59"
],
[
"f31a6d464301980a41367b8d14f880a3",
"XXXX.m line 46",
"-[XXXX XXX:XXXX:]",
"50",
"41"
],
[
"c149e1dfccecff848d551b501caf41cc",
"XXXX.m line 554",
"-[XXXX tableView:didSelectRowAtIndexPath:]",
"48",
"47"
],
[
"609e79f399b1e6727222a8dc75474788",
"Pinkoi",
"specialized JSONDecoder.decode<A>(_:from:)",
"47",
"38"
]
]
可以看到是一个二维阵列。
加上转发 Slack 的 Function:
在上述程式码下方继续加入新 Function。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
function sendTop10CrashToSlack() {
var iOSTop10Crashes = queryiOSTop10Crashes();
var top10Tasks = new Array();
for (var i = 0; i < iOSTop10Crashes.length ; i++) {
var issue_id = iOSTop10Crashes[i][0];
var issue_title = iOSTop10Crashes[i][1];
var issue_subtitle = iOSTop10Crashes[i][2];
var number_of_crashes = iOSTop10Crashes[i][3];
var number_of_impacted_user = iOSTop10Crashes[i][4];
var strip_title = issue_title.replace(/[\<\\|\>]/g, '');
var strip_subtitle = issue_subtitle.replace(/[\<\\|\>]/g, '');
top10Tasks.push("<https://console.firebase.google.com/u/1/project/你的ProjectID/crashlytics/app/你的专案ID/issues/"+issue_id+"\\|"+(i+1)+". Crash: "+number_of_crashes+" 次 ("+number_of_impacted_user+"人) - "+strip_title+" "+strip_subtitle+">");
}
var messages = top10Tasks.join("\n");
var payload = {
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":bug::bug::bug: iOS 近 7 天闪退问题排行榜 :bug::bug::bug:",
"emoji": true
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": messages
}
},
{
"type": "divider"
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "前往 Crashlytics 查看近 7 天纪录",
"emoji": true
},
"url": "https://console.firebase.google.com/u/1/project/你的ProjectID/crashlytics/app/你的专案ID/issues?time=last-seven-days&state=open&type=crash&tag=all"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "前往 Crashlytics 查看近 30 天纪录",
"emoji": true
},
"url": "https://console.firebase.google.com/u/1/project/你的ProjectID/crashlytics/app/你的专案ID/issues?time=last-thirty-days&state=open&type=crash&tag=all"
}
]
},
{
"type": "context",
"elements": [
{
"type": "plain_text",
"text": "Crash 次数及发生版本仅统计近 7 天之间数据,并非所有资料。",
"emoji": true
}
]
}
]
};
var slackWebHookURL = "https://hooks.slack.com/services/XXXXX"; //更换成你的 in-coming webhook url
UrlFetchApp.fetch(slackWebHookURL,{
method : 'post',
contentType : 'application/json',
payload : JSON.stringify(payload)
})
}
如果不知道怎么取得 in-cming WebHook URL 可以参考 此篇文章 的「取得 Incoming WebHooks App URL」章节。
测试&设定排程
此时你的 Google Apps Script 专案应该会有上述两个 Function。
接下来请在上方的选择「sendTop10CrashToSlack」Function,然后点击 Debug 或 Run 执行测试一次;因第一次执行需要完成身份验证,所以请至少执行过一次再进行下一步。
执行测试一次没问题后,可以开始设定排程自动执行:
于左方选择闹钟图案,再选择右下方「+ Add Trigger」。
第一个「Choose which function to run」(需要执行的 function 入口) 请改为 sendTop10CrashToSlack
,时间周期可依个人喜好设定。
⚠️⚠️⚠️ 请特别注意每次查询都会累积然后收费的,所以千万不要乱设定;否则可能被排程自动执行搞到破产。
完成
范例成果图
现在起,你只要在 Slack 上就能快速追踪当前 App 闪退问题;甚至直接在上面进行讨论。
App Crash-Free Users Rate?
如果你想追的是 App Crash-Free Users Rate,可参考下篇「 Crashlytics + Google Analytics 自动查询 App Crash-Free Users Rate 」
延伸阅读
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。