> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flashduty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Firebase Crashlytics 告警集成

> 通过一段 Cloud Functions 代码，把 Firebase Crashlytics 的新增致命问题、回归和速率告警同步到 Flashduty On-call。

Firebase Crashlytics 没有面向第三方的通用 Webhook：崩溃告警只能通过 **Firebase Alerts** 以 Eventarc 事件的形式发给一个 Cloud Function（2nd gen）。本集成提供一段 Cloud Function 代码，订阅 Crashlytics 的告警事件并转发给 Flashduty，覆盖以下事件：

| Firebase 事件 | 触发条件 |
| :- | :- |
| 新增致命问题（`onNewFatalIssuePublished`） | 出现一个新的致命崩溃问题 |
| 回归（`onRegressionAlertPublished`） | 已标记为已解决的问题再次出现 |
| 速率告警（`onVelocityAlertPublished`） | 某个问题在近期会话中的崩溃率超过阈值 |
| 新增非致命问题（`onNewNonfatalIssuePublished`） | 出现一个新的非致命问题 |
| 新增 ANR 问题（`onNewAnrIssuePublished`） | 出现一个新的“应用无响应”（Android ANR）问题 |

Firebase 还会发送**稳定性摘要**（`onStabilityDigestPublished`），按天汇总多个热门问题；这是一份周期性报告而非单个可处理的问题，本集成不会把它转成告警。

<div className="hide">
  ## 在 Flashduty On-call

  ***

  您可通过以下两种方式获取集成推送地址，任选其一即可。

  ### 使用专属集成

  1. 进入 Flashduty 控制台，选择 **协作空间**，打开一个协作空间
  2. 选择 **配置** → **集成数据** → **专属集成**，点击 **新增一个集成**
  3. 选择 **Firebase Crashlytics**，点击 **保存**
  4. 打开生成的集成卡片，复制 **推送地址**

  ### 使用共享集成

  1. 进入 Flashduty 控制台，选择 **集成中心 → 告警事件**
  2. 选择 **Firebase Crashlytics**，填写集成名称
  3. 配置默认路由并选择协作空间；创建后可在 **路由** 中增加更多规则
  4. 点击 **保存**，复制生成的 **推送地址**
</div>

## 在 Firebase 中配置

***

Crashlytics 告警函数运行在 Cloud Functions（2nd gen）之上，Firebase 项目需要在 **Blaze（按量付费）** 方案下才能部署，即使实际用量落在免费额度内也需要绑定结算账号。

<Steps>
  <Step title="准备 Cloud Functions 项目">
    如果项目还没有初始化 Cloud Functions，在项目根目录执行：

    ```bash theme={null}
    firebase init functions
    ```

    选择 JavaScript 或 TypeScript，安装依赖时保留默认的 `firebase-functions` 和 `firebase-admin`。
  </Step>

  <Step title="写入转发函数">
    把 `functions/index.js`（或对应的入口文件）替换或追加为以下内容，将 `FLASHDUTY_URL` 换成上一步复制的推送地址（包含 `?integration_key=...`）：

    ```js theme={null}
    const {
      onNewFatalIssuePublished,
      onRegressionAlertPublished,
      onVelocityAlertPublished,
      onNewNonfatalIssuePublished,
      onNewAnrIssuePublished,
    } = require("firebase-functions/v2/alerts/crashlytics");

    const FLASHDUTY_URL = "<推送地址>";

    async function forwardToFlashduty(event) {
      const issue = event.data.payload.issue;
      const body = {
        alert_type: event.alertType,
        app_id: event.appId,
        issue_id: issue.id,
        issue_title: issue.title,
        issue_subtitle: issue.subtitle,
        app_version: issue.appVersion,
        create_time: event.data.createTime,
      };

      if (event.data.payload.resolveTime) {
        body.resolve_time = event.data.payload.resolveTime;
      }
      if (event.data.payload.crashCount !== undefined) {
        body.crash_count = event.data.payload.crashCount;
        body.crash_percentage = event.data.payload.crashPercentage;
        body.first_version = event.data.payload.firstVersion;
      }

      await fetch(FLASHDUTY_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
    }

    exports.flashdutyOnNewFatalIssue = onNewFatalIssuePublished((event) => forwardToFlashduty(event));
    exports.flashdutyOnRegression = onRegressionAlertPublished((event) => forwardToFlashduty(event));
    exports.flashdutyOnVelocity = onVelocityAlertPublished((event) => forwardToFlashduty(event));
    exports.flashdutyOnNewNonfatalIssue = onNewNonfatalIssuePublished((event) => forwardToFlashduty(event));
    exports.flashdutyOnNewAnrIssue = onNewAnrIssuePublished((event) => forwardToFlashduty(event));
    ```

    字段名和取值路径都来自 `firebase-functions` 的官方类型定义，不要改名：`event.alertType`、`event.appId`、`event.data.createTime` 和 `event.data.payload.issue` 是每种事件共有的字段，`resolveTime` 只在回归事件中存在，`crashCount`/`crashPercentage`/`firstVersion` 只在速率告警中存在。函数使用 Node.js 18 及以上运行时内置的 `fetch`，不需要额外安装 HTTP 客户端库。

    如果只想接入其中几类事件，删掉不需要的 `require` 项和对应的 `exports` 行即可；不要修改保留下来的字段名。
  </Step>

  <Step title="按需限定应用">
    如果 Firebase 项目下有多个应用（iOS、Android、Web 各一个 App ID），且只想接入其中一个，把 `onXxxPublished(handler)` 换成 `onXxxPublished(appId, handler)`，`appId` 取自 **项目设置 → 常规 → 应用的应用 ID**。不传 `appId` 时，项目下所有应用的事件都会转发。
  </Step>

  <Step title="部署">
    ```bash theme={null}
    firebase deploy --only functions:flashdutyOnNewFatalIssue,functions:flashdutyOnRegression,functions:flashdutyOnVelocity,functions:flashdutyOnNewNonfatalIssue,functions:flashdutyOnNewAnrIssue
    ```

    首次部署 Firebase Alerts 触发的函数可能需要几分钟启用 Eventarc 相关服务，属于正常现象。
  </Step>

  <Step title="验证">
    Crashlytics 告警没有测试按钮，用以下两种方式分别验证：

    * **验证 Flashduty 一侧**：用上面模板中的字段结构，向推送地址发一次 curl 请求确认能建立告警：

      ```bash theme={null}
      curl -X POST "<推送地址>" \
        -H "Content-Type: application/json" \
        -d '{"alert_type":"crashlytics.newFatalIssue","app_id":"test","issue_id":"test-issue","issue_title":"Test issue","app_version":"1.0.0"}'
      ```

      验证后请在 Flashduty 中手动关闭这条测试告警。

    * **验证完整链路**：在一个 Debug 构建中触发一次真实崩溃（例如调用 `Crashlytics.crash()` / `FirebaseCrashlytics.getInstance().log(...)` 后强制抛异常），等待该问题出现在 Crashlytics 控制台（可能有几分钟延迟），确认 Flashduty 收到对应告警。
  </Step>

  <Step title="开启超时自动关闭">
    Crashlytics 不会为这些事件发送“已解决”通知，Flashduty 中的告警不会自动恢复。请在接收该集成告警的协作空间中开启 **超时自动关闭**，超时计时起点选择 **故障触发**，超时时长建议设置为 **72 小时**（崩溃问题的排查和发版周期通常比一般基础设施故障更长），配置方法参阅 [配置协作空间](/zh/on-call/channel/create-edit)。问题修复并发布新版本后，可以在 Flashduty 中手动关闭告警。
  </Step>
</Steps>

## Alert Key

***

Flashduty 使用事件类型 `alert_type`、应用 ID `app_id` 和 Crashlytics 的问题 ID `issue_id`（对应官方 `Issue.id`）共同计算 Alert Key。一个 Cloud Function 部署（一个推送地址）可以转发项目下所有应用的事件（见上文“按需限定应用”），而 Firebase 并未说明 `issue_id` 在不同应用之间也互不重复，因此 `app_id` 也参与计算，避免两个应用凑巧拿到相同的问题 ID 时被合并成一条告警。因此：

* 同一应用、同一问题的同一类事件（例如两次 `newFatalIssue` 投递，Eventarc 可能重试）落在同一条告警上
* 同一应用、同一问题先后触发新增致命问题和速率告警，是两条独立的告警
* 一个曾被标记为已解决的问题再次出现（`regression`）会开一条新的告警，不会复用之前 `newFatalIssue` 的那条
* 问题标题、副标题、应用版本号和时间戳的变化都不会改变 Alert Key

请求缺少 `issue_id` 时，Flashduty 会拒绝该请求。

## 告警等级

***

Crashlytics 的告警事件不携带等级，Flashduty 按事件类型确定等级：

| 事件类型（`alert_type`） | Flashduty 等级 |
| :- | :- |
| `crashlytics.newFatalIssue` | **Critical** |
| `crashlytics.regression` | **Critical** |
| `crashlytics.velocity` | Warning |
| `crashlytics.newNonfatalIssue` | Warning |
| `crashlytics.newAnrIssue` | Warning |

如需统一改为其他等级，在推送地址后追加 `&severity=Critical`（或 `Warning`、`Info`）。

## 告警内容

***

告警标题为 `Firebase Crashlytics <事件类型>: <issue_title>`，如 `Firebase Crashlytics new fatal issue: java.lang.NullPointerException`。描述包含问题副标题（崩溃发生的类和方法）、应用版本号；回归事件额外包含上次被标记解决的时间，速率告警额外包含崩溃会话数、崩溃率和首次出现的版本号。

| 标签 | 来源 |
| :- | :- |
| `alert_type` | Firebase 的事件类型，如 `crashlytics.newFatalIssue` |
| `app_id` | Firebase 应用 ID |
| `issue_id` | Crashlytics 问题 ID |
| `issue_subtitle` | 问题副标题（崩溃位置） |
| `app_version` | 出问题时的应用版本号 |
| `create_time` | 事件创建时间 |
| `resolve_time` | 仅回归事件：问题上次被标记解决的时间 |
| `crash_count` / `crash_percentage` / `first_version` | 仅速率告警：崩溃会话数、崩溃率、首次出现的版本号 |

## 排查问题

***

* **函数部署失败或报权限错误**：确认 Firebase 项目已开通 Blaze 方案，且执行部署的账号对该项目有 Cloud Functions 部署权限
* **崩溃已发生，但迟迟没有告警**：Crashlytics 处理崩溃日志和触发告警函数通常有几分钟延迟；用 `firebase functions:log` 查看对应函数有没有被触发、`fetch` 有没有抛错
* **Flashduty 返回参数错误**：多数情况是模板中的字段名被改动，或推送地址中的 `integration_key` 不属于本集成；对照上面的模板逐字核对
* **告警一直不恢复**：Crashlytics 的这些事件都不带恢复信号，请开启协作空间的超时自动关闭，或在问题修复后手动关闭

更多字段含义请参阅 Firebase 官方文档 [Trigger a function on Crashlytics events](https://firebase.google.com/docs/functions/beta/alerts/crashlytics) 和 `firebase-functions` 源码 [`alerts/crashlytics`](https://github.com/firebase/firebase-functions/blob/master/src/v2/providers/alerts/crashlytics.ts)。
