Skip to content
34 changes: 34 additions & 0 deletions src/commands/crashlytics-events-batchget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as clc from "colorette";

import { Command } from "../command";
import { FirebaseError } from "../error";
import { Options } from "../options";
import { logger } from "../logger";
import { requireAuth } from "../requireAuth";
import { batchGetEvents } from "../crashlytics/events";
import { requireAppId, renderEventsTable } from "../crashlytics/utils";

interface CommandOptions extends Options {
app?: string;
}

export const command = new Command("crashlytics:events:batchget <eventNames...>")
.description("get specific Crashlytics events by resource name")
.before(requireAuth)
.option("--app <appId>", "the app id of your Firebase app")
.action(async (eventNames: string[], options: CommandOptions) => {
const appId = requireAppId(options.app);
if (!eventNames || eventNames.length === 0) {
throw new FirebaseError("provide at least one event resource name");
}

const result = await batchGetEvents(appId, eventNames);

if (!result.events || result.events.length === 0) {
logger.info(clc.bold("No events found."));
} else {
renderEventsTable(result.events);
}

return result;
});
50 changes: 50 additions & 0 deletions src/commands/crashlytics-events-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as clc from "colorette";

import { Command } from "../command";
import { FirebaseError } from "../error";
import { Options } from "../options";
import { logger } from "../logger";
import { requireAuth } from "../requireAuth";
import { listEvents } from "../crashlytics/events";
import { EventFilter } from "../crashlytics/filters";
import { requireAppId, renderEventsTable } from "../crashlytics/utils";

interface CommandOptions extends Options {
app?: string;
issueId?: string;
issueVariantId?: string;
pageSize?: number;
}

export const command = new Command("crashlytics:events:list")
.description("list recent Crashlytics events for an issue")
.before(requireAuth)
.option("--app <appId>", "the app id of your Firebase app")
.option("--issue-id <issueId>", "filter by issue id")
.option("--issue-variant-id <variantId>", "filter by issue variant id")
.option("--page-size <number>", "number of events to return", 1)
.action(async (options: CommandOptions) => {
const appId = requireAppId(options.app);
if (!options.issueId && !options.issueVariantId) {
throw new FirebaseError("set --issue-id or --issue-variant-id to filter events");
}

const filter: EventFilter = {};
if (options.issueId) {
filter.issueId = options.issueId;
}
if (options.issueVariantId) {
filter.issueVariantId = options.issueVariantId;
}

const pageSize = options.pageSize ?? 1;
const result = await listEvents(appId, filter, pageSize);

if (!result.events || result.events.length === 0) {
logger.info(clc.bold("No events found."));
} else {
renderEventsTable(result.events);
}

return result;
});
42 changes: 42 additions & 0 deletions src/commands/crashlytics-issues-get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as Table from "cli-table3";

import { Command } from "../command";
import { Options } from "../options";
import { logger } from "../logger";
import { requireAuth } from "../requireAuth";
import { getIssue } from "../crashlytics/issues";
import { requireAppId } from "../crashlytics/utils";

interface CommandOptions extends Options {
app?: string;
}

export const command = new Command("crashlytics:issues:get <issueId>")
.description("get details for a Crashlytics issue")
.before(requireAuth)
.option("--app <appId>", "the app id of your Firebase app")
.action(async (issueId: string, options: CommandOptions) => {
const appId = requireAppId(options.app);

const issue = await getIssue(appId, issueId);

// Display formatted output
const table = new Table();
table.push(
{ ID: issue.id || "-" },
{ Title: issue.title || "-" },
{ Subtitle: issue.subtitle || "-" },
{ Type: issue.errorType || "-" },
{ State: issue.state || "-" },
{ "First Seen": issue.firstSeenVersion || "-" },
{ "Last Seen": issue.lastSeenVersion || "-" },
{ Variants: issue.variants?.length?.toString() || "0" },
);
logger.info(table.toString());

if (issue.uri) {
logger.info(`\nConsole: ${issue.uri}`);
}

return issue;
});
37 changes: 37 additions & 0 deletions src/commands/crashlytics-issues-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Command } from "../command";
import { FirebaseError } from "../error";
import { Options } from "../options";
import * as utils from "../utils";
import { requireAuth } from "../requireAuth";
import { updateIssue } from "../crashlytics/issues";
import { State } from "../crashlytics/types";
import { requireAppId } from "../crashlytics/utils";

interface CommandOptions extends Options {
app?: string;
state?: string;
}

export const command = new Command("crashlytics:issues:update <issueId>")
.description("update the state of a Crashlytics issue")
.before(requireAuth)
.option("--app <appId>", "the app id of your Firebase app")
.option("--state <state>", "the new state for the issue (OPEN or CLOSED)")
.action(async (issueId: string, options: CommandOptions) => {
const appId = requireAppId(options.app);
if (!options.state) {
throw new FirebaseError("set --state to OPEN or CLOSED");
}

const stateUpper = options.state.toUpperCase();
if (stateUpper !== "OPEN" && stateUpper !== "CLOSED") {
throw new FirebaseError("--state must be OPEN or CLOSED");
}

const state = stateUpper as State;
const issue = await updateIssue(appId, issueId, state);

utils.logLabeledSuccess("crashlytics", `Issue ${issueId} is now ${String(issue.state)}`);

return issue;
});
30 changes: 30 additions & 0 deletions src/commands/crashlytics-notes-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Command } from "../command";
import { FirebaseError } from "../error";
import { Options } from "../options";
import * as utils from "../utils";
import { requireAuth } from "../requireAuth";
import { createNote } from "../crashlytics/notes";
import { requireAppId } from "../crashlytics/utils";

interface CommandOptions extends Options {
app?: string;
note?: string;
}

export const command = new Command("crashlytics:notes:create <issueId>")
.description("add a note to a Crashlytics issue")
.before(requireAuth)
.option("--app <appId>", "the app id of your Firebase app")
.option("--note <text>", "the note text to add to the issue")
.action(async (issueId: string, options: CommandOptions) => {
const appId = requireAppId(options.app);
if (!options.note) {
throw new FirebaseError("set --note <text> to specify the note content");
}

const note = await createNote(appId, issueId, options.note);

utils.logLabeledSuccess("crashlytics", `Created note on issue ${issueId}`);

return note;
});
21 changes: 21 additions & 0 deletions src/commands/crashlytics-notes-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Command } from "../command";
import { Options } from "../options";
import * as utils from "../utils";
import { requireAuth } from "../requireAuth";
import { deleteNote } from "../crashlytics/notes";
import { requireAppId } from "../crashlytics/utils";

interface CommandOptions extends Options {
app?: string;
}

export const command = new Command("crashlytics:notes:delete <issueId> <noteId>")
.description("delete a note from a Crashlytics issue")
.before(requireAuth)
.option("--app <appId>", "the app id of your Firebase app")
.action(async (issueId: string, noteId: string, options: CommandOptions) => {
const appId = requireAppId(options.app);

await deleteNote(appId, issueId, noteId);
utils.logLabeledSuccess("crashlytics", `Deleted note ${noteId} from issue ${issueId}`);
});
48 changes: 48 additions & 0 deletions src/commands/crashlytics-notes-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as clc from "colorette";
import * as Table from "cli-table3";

import { Command } from "../command";
import { Options } from "../options";
import { logger } from "../logger";
import { requireAuth } from "../requireAuth";
import { listNotes } from "../crashlytics/notes";
import { requireAppId } from "../crashlytics/utils";

interface CommandOptions extends Options {
app?: string;
pageSize?: number;
}

export const command = new Command("crashlytics:notes:list <issueId>")
.description("list notes for a Crashlytics issue")
.before(requireAuth)
.option("--app <appId>", "the app id of your Firebase app")
.option("--page-size <number>", "number of notes to return", 20)
.action(async (issueId: string, options: CommandOptions) => {
const appId = requireAppId(options.app);

const pageSize = options.pageSize ?? 20;
const notes = await listNotes(appId, issueId, pageSize);

if (notes.length === 0) {
logger.info(clc.bold("No notes found."));
} else {
const table = new Table({
head: ["Author", "Created", "Note"],
style: { head: ["green"] },
colWidths: [30, 25, 50],
wordWrap: true,
});
for (const note of notes) {
table.push([
note.author || "-",
note.createTime ? new Date(note.createTime).toLocaleString() : "-",
note.body || "-",
]);
}
logger.info(table.toString());
logger.info(`\n${notes.length} note(s).`);
}

return notes;
});
Loading