---
name: crashlytics-events-to-excel
description: Use when exporting Firebase Crashlytics event-level data (custom keys, device, stack trace) to a multi-sheet Excel workbook for sharing with non-engineers — works via Firebase Console's internal API replayed from browser cURLs, when BigQuery export isn't available
---

# Crashlytics Events to Excel

> From Shubham Singh Bisht's Android helpers — https://shubhamsinghbisht.com/android-roadmap/#helpers

## Overview

Firebase Console shows per-event custom keys in the UI but offers no bulk download. The undocumented `crashlytics-pa.clients6.google.com` API the Console calls returns full event JSON if you replay the user's authenticated browser session. This skill turns N issues' events into one workbook with **one sheet per issue plus combined pivots**.

## When to Use

- User wants Crashlytics event data in Excel for sharing
- User has Firebase Console access but no BigQuery `firebase_crashlytics` export
- Need to compare multiple issues / aggregate by device, OS, custom key, user
- Need a snapshot of current Crashlytics state for a report

## When NOT to Use

- BigQuery export is enabled: query that, it's faster and not auth-bound
- Need live/streaming data: this is point-in-time
- User can't log into Firebase Console for that project

## What to Ask the User For

Two cURLs, copied from Chrome/Brave DevTools (Network tab, right-click → Copy → Copy as cURL) while logged into Firebase Console:

1. **List events cURL**: open one issue's detail page, reload, find the `metrics:listSessionEventIds` request, copy as cURL.
2. **Event detail cURL**: click any event row, find the `processedevents/{sessionId}_{eventId}` request, copy as cURL.

For multi-issue export, also ask for the **other issue IDs** (each issue's URL contains its ID). Same auth works across all issues in the same Firebase project.

cURLs are **very** short-lived. The `SAPISIDHASH` itself lasts about an hour, but Google's session continuity cookies (`SIDCC`, `__Secure-1PSIDCC`, `__Secure-3PSIDCC`, `__Secure-1PSIDTS`, `__Secure-3PSIDTS`) rotate every few minutes. Once they're stale the server replies 401 with `reason: SESSION_COOKIE_INVALID` even if the SAPISIDHASH is still in its hour window. **Run the script within ~2 minutes of the user copying the cURL**, or ask them to re-copy right before you fire.

## Workflow

1. Parse the cURLs to extract these constants:
   - `PROJECT_NUMBER`: from the URL path (`/projects/<PROJECT_NUMBER>/…`)
   - `APP_ID`: from the URL path (e.g. `android:com.example.app`)
   - `KEY`: the `key=` query param
   - `authorization` header (full `SAPISIDHASH ...` value)
   - `cookie` string (`-b '...'`)
2. Write a working folder (e.g. `/tmp/crashlytics_export/`), drop the constants into `auth.json` there (keys: `project_number`, `app_id`, `key`, `authorization`, `cookie`), and copy `crashlytics_to_excel.py` from this skill into it.
3. Edit the `ISSUES` list at the top of the script: one entry per issue with `id`, `label`, `tag` (used as sheet name), and `custom_keys` (the keys you expect in that issue's subtitle). Optionally set `GROUP_KEY` (a custom key to pivot on), `SIZE_KEY` (a byte-count key to sum) and `ERROR_KEYS`.
4. Adjust `INTERVAL` and `VERSION_FILTERS` at the top of the script to match the slice of interest.
5. Run `python3 crashlytics_to_excel.py` (needs `openpyxl`; `--auth` and `--out` override the default paths). It pages through `listSessionEventIds` per issue, fetches each `processedevents/{id}`, then writes the workbook.
6. Deliver the `.xlsx` and **delete `auth.json`** plus the intermediate `events_*.json`: those contain session credentials and PII.

## Output Workbook Structure

- **One sheet per issue** (sheet name = `tag`): raw events with that issue's specific custom keys as columns, plus device/OS/user/top-frame metadata
- **`All_Events` combined sheet**: every event across all issues, with an `Issue Type` column for filtering and every issue's custom keys as columns. Sorted by Event Time.
- **`Insights` sheet**: counts pivots by Issue Type, Company (OEM), Marketing Name, OS Version, Specific Error, User ID, and `GROUP_KEY` if set. Plus users (and `GROUP_KEY` values) hit by multiple issue types, and the total of `SIZE_KEY` if set.

All data sheets have frozen header row + auto-filter.

## Parsing Custom Keys

The script parses custom keys out of `stacktraceGroup.exceptions[0].stacktrace.caption.subtitle`, which is a comma-separated `key=value` string usually prefixed with `Title: ` (a colon). `parse_subtitle_kv` strips the prefix and splits. If a particular issue uses a different format, print one raw event from the intermediate JSON and inspect before adjusting.

## Pagination Detail (Non-Obvious)

`listSessionEventIds` does **not** use a `nextPageToken` field. To page, set `pageToken` in the request body to the **last entry's `{sessionId, eventId}`** from the previous page. Loop exits when no new keys are added (the API returns overlapping pages on the boundary). This is already correct in the template; don't "fix" it to use a `nextPageToken` field.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| 401 / "Unauthorized" mid-run | `SAPISIDHASH` expired. Ask for fresh cURLs, rerun. |
| Empty `parsed` dict on every event | That issue doesn't use custom keys, or the subtitle format differs. Print one raw event and inspect before adjusting. |
| Same events looping forever in pagination | Don't switch to a `nextPageToken` field; it doesn't exist. The positional `{sessionId, eventId}` token is correct. |
| Committing `auth.json` to git | `.gitignore` it. Delete after use: those are live Google session cookies. |
| Date range too wide → thousands of events, slow | Tighten `interval.startTime` / `endTime` to the window the user cares about. |

## Security Cleanup

After delivering the workbook, delete `auth.json` and any `events_*.json` from the working folder. The cookie + SAPISIDHASH grant full access to the user's Google session.
