Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -406,3 +406,6 @@ Mentra-Zephyr-Glasses-Client/

# Navigation miniapp build output
miniapps/navigation/build/

# Downloaded bug report bundles (scripts/fetch-incident-logs.sh)
incident-logs/
28 changes: 15 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,28 +168,30 @@ Automated ransomware scanners actively target exposed MongoDB instances. Use Mon

## Bug Report Logs

When working on bug reports linked to `console.mentra.glass/admin/incidents/{id}`:
Bug reports and feedback filed from the Mentra App land in the Cloud V2 reports system. Report ids look like `rep_01...` and appear in the reports Slack notifications and in the admin console's Incident system page (admin.mentraglass.com).

1. Extract the incident ID from the URL
2. Fetch logs: `./scripts/fetch-incident-logs.sh {incidentId}` — or use the **mentra-console** MCP server (`cloud/packages/console-mcp`, tools `incident_get` / `incident_get_logs`) from Cursor
3. Requires `MENTRA_AGENT_API_KEY` in your environment (or in MCP `env` / `~/.zshrc` when using `scripts/run-mcp.sh`)
1. Get the report id (from Slack, the admin console, or the user)
2. Fetch it: `./scripts/fetch-incident-logs.sh {reportId}` — downloads `report.json` plus every artifact into `./incident-logs/{reportId}/`
3. Requires `MENTRA_ADMIN_TOKEN` in your environment: an org API key (`msk_...`) whose synthetic email is allowlisted via `CLOUD_CORE_ADMIN_EMAILS`, or a WorkOS access token of an admin user
4. Defaults to prod; use `--env dev|staging` or `MENTRA_CORE_URL` for other environments

The logs JSON contains:
What you get:

- `phoneLogs` - Last 10 min of mobile app logs
- `cloudLogs` - Last 10 min of cloud service logs
- `glassesLogs` - Last 10 min of glasses logs (if available)
- `appTelemetryLogs` - Logs from third-party apps (if telemetry enabled)
- `phoneState` - Snapshot of app state at time of report
- `feedback` - User's bug report description
- `report.json` - Full report document: `kind` (bug/feedback/automatic), `trigger`, `report` (actual/expected behavior, severity, contact email), `feedback`, `context` (phone/glasses/app state snapshot), artifact metadata, and asset rows
- `NN-logs-{source}.json` - Log bundles uploaded by the devices (e.g. phone, glasses), each `{entries: [{timestamp, level, message, source?}]}`
- `NN-screenshot-phone-*.{png,jpg}` - Screenshots attached by the user

Other modes: `--json` prints the raw report JSON to stdout (no downloads); `--list [--kind ...] [--status ...] [--limit N]` lists recent reports.

Example:

```bash
export MENTRA_AGENT_API_KEY=your-api-key
./scripts/fetch-incident-logs.sh 550e8400-e29b-41d4-a716-446655440000
export MENTRA_ADMIN_TOKEN=msk_your-admin-key
./scripts/fetch-incident-logs.sh rep_01JZWY3V8N0F2E9GQ4T6KXH5RD
```

Note: the **mentra-console** MCP server (`cloud/packages/console-mcp`, tools `incident_get` / `incident_get_logs`) still targets the legacy V1 incidents API (`/api/agent/incidents`, `X-Agent-Key`) and has not been ported to the V2 reports API yet.

## Additional Documentation

- Mintlify docs: `/mintlify-docs/`
Expand Down
224 changes: 198 additions & 26 deletions scripts/fetch-incident-logs.sh
Original file line number Diff line number Diff line change
@@ -1,35 +1,207 @@
#!/bin/bash
# scripts/fetch-incident-logs.sh
# Fetch incident logs for debugging bug reports
# Usage: ./scripts/fetch-incident-logs.sh <incidentId>

set -e

INCIDENT_ID=$1
API_HOST="${MENTRA_API_HOST:-https://api.mentra.glass}"

if [ -z "$INCIDENT_ID" ]; then
echo "Usage: ./scripts/fetch-incident-logs.sh <incidentId>"
echo "Example: ./scripts/fetch-incident-logs.sh 01HXYZ..."
echo ""
echo "Required environment variables:"
echo " MENTRA_AGENT_API_KEY - API key for agent access"
echo ""
echo "Optional environment variables:"
echo " MENTRA_API_HOST - API host (default: https://api.mentra.glass)"
# Fetch a Cloud V2 bug report and its artifacts (log bundles, screenshots)
# for debugging. Reports are filed from the Mentra App and stored by the
# Cloud V2 reports service; this script reads them back through the admin
# reports API (GET /api/admin/reports/...).
#
# Usage:
# ./scripts/fetch-incident-logs.sh <reportId> [options] Download report + artifacts
# ./scripts/fetch-incident-logs.sh --list [options] List recent reports (JSON)
#
# Options:
# -o, --out DIR Output directory (default: ./incident-logs/<reportId>)
# --json Print the raw report JSON to stdout, skip artifact downloads
# --env ENV prod | staging | dev (default: prod)
# --kind KIND (--list) bug | feedback | automatic
# --status STATUS (--list) collecting | ready | closed
# --limit N (--list) max reports to return (1-200, default 50)
#
# Environment variables:
# MENTRA_ADMIN_TOKEN (required) Bearer token for the admin API: an org API
# key (msk_...) whose synthetic email is allowlisted in
# CLOUD_CORE_ADMIN_EMAILS, or a WorkOS access token of
# an admin user.
# MENTRA_CORE_URL (optional) Core API base URL; overrides --env.

set -euo pipefail

usage() {
sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'
exit 1
}

err() { echo "Error: $*" >&2; }
note() { echo "$*" >&2; }

command -v curl >/dev/null || { err "curl is required"; exit 1; }
command -v jq >/dev/null || { err "jq is required (brew install jq)"; exit 1; }

REPORT_ID=""
OUT_DIR=""
MODE="fetch"
ENV_NAME="prod"
LIST_KIND=""
LIST_STATUS=""
LIST_LIMIT=""

while [ $# -gt 0 ]; do
case "$1" in
--list) MODE="list" ;;
--json) MODE="json" ;;
-o|--out) OUT_DIR="${2:?--out requires a directory}"; shift ;;
--env) ENV_NAME="${2:?--env requires prod|staging|dev}"; shift ;;
--kind) LIST_KIND="${2:?--kind requires a value}"; shift ;;
--status) LIST_STATUS="${2:?--status requires a value}"; shift ;;
--limit) LIST_LIMIT="${2:?--limit requires a number}"; shift ;;
-h|--help) usage ;;
-*) err "unknown option: $1"; usage ;;
*)
if [ -n "$REPORT_ID" ]; then err "unexpected argument: $1"; usage; fi
REPORT_ID="$1"
;;
esac
shift
done

case "$ENV_NAME" in
prod) DEFAULT_CORE_URL="https://core.mentraglass.com" ;;
staging) DEFAULT_CORE_URL="https://core.staging.us-west-2.mentraglass.com" ;;
dev) DEFAULT_CORE_URL="https://core.dev.us-west-2.mentraglass.com" ;;
*) err "--env must be prod, staging, or dev (got: $ENV_NAME)"; exit 1 ;;
esac
CORE_URL="${MENTRA_CORE_URL:-$DEFAULT_CORE_URL}"
CORE_URL="${CORE_URL%/}"

if [ "$MODE" != "list" ] && [ -z "$REPORT_ID" ]; then
usage
fi
if [ -n "$REPORT_ID" ] && ! printf '%s' "$REPORT_ID" | grep -Eq '^[A-Za-z0-9_-]+$'; then
err "report id contains unexpected characters: $REPORT_ID"
exit 1
fi

if [ -z "$MENTRA_AGENT_API_KEY" ]; then
echo "Error: MENTRA_AGENT_API_KEY environment variable not set"
echo ""
echo "To set this:"
echo " export MENTRA_AGENT_API_KEY=your-api-key"
if [ -z "${MENTRA_ADMIN_TOKEN:-}" ]; then
err "MENTRA_ADMIN_TOKEN environment variable not set"
note ""
note "The admin reports API needs a bearer token with admin access:"
note " - an org API key (msk_...) allowlisted via CLOUD_CORE_ADMIN_EMAILS, or"
note " - a WorkOS access token of an admin user"
note ""
note " export MENTRA_ADMIN_TOKEN=msk_..."
exit 1
fi

echo "Fetching incident logs for: $INCIDENT_ID" >&2
echo "API host: $API_HOST" >&2
# api_get PATH OUTFILE -> echoes HTTP status; body lands in OUTFILE.
api_get() {
curl -sS -m 60 \
-H "Authorization: Bearer $MENTRA_ADMIN_TOKEN" \
-H "Accept: application/json" \
-o "$2" -w '%{http_code}' \
"$CORE_URL$1"
}

curl -s -H "X-Agent-Key: $MENTRA_AGENT_API_KEY" \
"$API_HOST/api/agent/incidents/$INCIDENT_ID/logs" | jq .
fail_for_status() {
local status="$1" body="$2" what="$3"
case "$status" in
2??) return 0 ;;
401) err "unauthorized (401) fetching $what — MENTRA_ADMIN_TOKEN was rejected" ;;
403) err "forbidden (403) fetching $what — token is valid but not admin-allowlisted (CLOUD_CORE_ADMIN_EMAILS)" ;;
404) err "not found (404) fetching $what — wrong report id, or this environment does not serve the admin reports API yet" ;;
*) err "HTTP $status fetching $what" ;;
esac
if [ -s "$body" ]; then jq . "$body" >&2 2>/dev/null || cat "$body" >&2; fi
exit 1
}

TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT

if [ "$MODE" = "list" ]; then
QUERY=""
[ -n "$LIST_KIND" ] && QUERY="$QUERY&kind=$LIST_KIND"
[ -n "$LIST_STATUS" ] && QUERY="$QUERY&status=$LIST_STATUS"
[ -n "$LIST_LIMIT" ] && QUERY="$QUERY&limit=$LIST_LIMIT"
QUERY="${QUERY#&}"
note "Listing reports from $CORE_URL${QUERY:+ ($QUERY)}"
BODY="$TMP_DIR/list.json"
STATUS=$(api_get "/api/admin/reports${QUERY:+?$QUERY}" "$BODY")
fail_for_status "$STATUS" "$BODY" "report list"
jq . "$BODY"
exit 0
fi

note "Fetching report $REPORT_ID from $CORE_URL"
DETAIL="$TMP_DIR/detail.json"
STATUS=$(api_get "/api/admin/reports/$REPORT_ID" "$DETAIL")
fail_for_status "$STATUS" "$DETAIL" "report $REPORT_ID"

if [ "$MODE" = "json" ]; then
jq . "$DETAIL"
exit 0
fi

OUT_DIR="${OUT_DIR:-./incident-logs/$REPORT_ID}"
mkdir -p "$OUT_DIR"
jq . "$DETAIL" > "$OUT_DIR/report.json"

KIND=$(jq -r '.report.kind' "$DETAIL")
REPORT_STATUS=$(jq -r '.report.status' "$DETAIL")
ARTIFACT_COUNT=$(jq '.report.artifacts | length' "$DETAIL")
note "Report kind: $KIND, status: $REPORT_STATUS, artifacts: $ARTIFACT_COUNT"
if [ "$REPORT_STATUS" = "collecting" ]; then
note "Note: status is 'collecting' — the device may still be uploading artifacts"
fi

ext_for_content_type() {
case "${1%%;*}" in
application/json) echo "json" ;;
image/png) echo "png" ;;
image/jpeg) echo "jpg" ;;
image/webp) echo "webp" ;;
image/gif) echo "gif" ;;
text/*) echo "txt" ;;
*) echo "bin" ;;
esac
}

# Artifact type/source/filename are report metadata (source and filename are
# client-supplied free text), so strip anything path-like before they can
# shape a local filename.
sanitize_component() {
printf '%s' "$1" | tr -cd 'A-Za-z0-9._-' | cut -c1-40
}

FAILED=0
INDEX=0
while IFS=$'\t' read -r ARTIFACT_ID TYPE SOURCE CONTENT_TYPE FILENAME; do
[ -n "$ARTIFACT_ID" ] || continue
INDEX=$((INDEX + 1))
EXT=$(ext_for_content_type "$CONTENT_TYPE")
SAFE_TYPE=$(sanitize_component "$TYPE")
SAFE_SOURCE=$(sanitize_component "$SOURCE")
SAFE_NAME=$(sanitize_component "$FILENAME")
SAFE_NAME="${SAFE_NAME%.*}"
FILE=$(printf '%02d-%s-%s%s.%s' "$INDEX" "${SAFE_TYPE:-artifact}" "${SAFE_SOURCE:-unknown}" "${SAFE_NAME:+-$SAFE_NAME}" "$EXT")
BODY="$TMP_DIR/artifact"
STATUS=$(api_get "/api/admin/reports/$REPORT_ID/artifacts/$ARTIFACT_ID" "$BODY") || STATUS="000"
if ! printf '%s' "$STATUS" | grep -q '^2'; then
err "artifact $ARTIFACT_ID ($TYPE/$SOURCE) failed with HTTP $STATUS — skipping"
FAILED=$((FAILED + 1))
continue
fi
if [ "$EXT" = "json" ] && jq . "$BODY" > "$TMP_DIR/pretty" 2>/dev/null; then
mv "$TMP_DIR/pretty" "$OUT_DIR/$FILE"
else
mv "$BODY" "$OUT_DIR/$FILE"
fi
note " saved $FILE"
done < <(jq -r '.report.artifacts[] | [.artifactId, .type, .source, (.contentType // ""), (.filename // "")] | @tsv' "$DETAIL")

echo "$OUT_DIR"
ls -lh "$OUT_DIR" >&2

if [ "$FAILED" -gt 0 ]; then
err "$FAILED artifact(s) failed to download"
exit 1
fi
Loading