Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
215 changes: 189 additions & 26 deletions scripts/fetch-incident-logs.sh
Original file line number Diff line number Diff line change
@@ -1,35 +1,198 @@
#!/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_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

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"
# 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"
}

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

echo "Fetching incident logs for: $INCIDENT_ID" >&2
echo "API host: $API_HOST" >&2
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"

curl -s -H "X-Agent-Key: $MENTRA_AGENT_API_KEY" \
"$API_HOST/api/agent/incidents/$INCIDENT_ID/logs" | jq .
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
}

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_NAME=$(printf '%s' "$FILENAME" | tr -cd 'A-Za-z0-9._-' | cut -c1-40)
SAFE_NAME="${SAFE_NAME%.*}"
FILE=$(printf '%02d-%s-%s%s.%s' "$INDEX" "$TYPE" "$SOURCE" "${SAFE_NAME:+-$SAFE_NAME}" "$EXT")
Comment thread
PhilippeFerreiraDeSousa marked this conversation as resolved.
Outdated
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