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
77 changes: 39 additions & 38 deletions packages/cli/src/cli/commands/project/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Option } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, normalizeDatetime } from "@/cli/utils/index.js";
import { ApiError, InvalidInputError } from "@/core/errors.js";
import { readProjectConfig } from "@/core/index.js";
import type {
FunctionLogFilters,
FunctionLogsResponse,
Expand Down Expand Up @@ -188,27 +187,30 @@ async function fetchLogsForFunctions(
try {
logs = await fetchFunctionLogs(functionName, filters);
} catch (error) {
if (
error instanceof ApiError &&
error.statusCode === 404 &&
availableFunctionNames.length > 0
) {
const available = availableFunctionNames.join(", ");
throw new InvalidInputError(
`Function "${functionName}" was not found in this app`,
{
hints: [
{
message: `Available functions in this project: ${available}`,
},
{
message:
"Make sure the function has been deployed before fetching logs",
command: "base44 functions deploy",
},
],
},
if (error instanceof ApiError && error.statusCode === 404) {
const namesForHint = await getFunctionNamesForHint(
availableFunctionNames,
error,
);
if (namesForHint.length > 0) {
const available = namesForHint.join(", ");
throw new InvalidInputError(
`Function "${functionName}" was not found in this app`,
{
cause: error,
hints: [
{
message: `Available functions in this app: ${available}`,
},
{
message:
"Make sure the function has been deployed before fetching logs",
command: "base44 functions deploy",
},
],
},
);
}
}
throw error;
}
Expand All @@ -235,16 +237,23 @@ async function fetchLogsForFunctions(
return allEntries;
}

async function getProjectFunctionNames(projectRoot: string): Promise<string[]> {
const { functions } = await readProjectConfig(projectRoot);
return functions.map((fn) => fn.name);
}

async function getRemoteFunctionNames(): Promise<string[]> {
const { functions } = await listDeployedFunctions();
return functions.map((fn) => fn.name);
}

async function getFunctionNamesForHint(
availableFunctionNames: string[],
logsError: ApiError,
): Promise<string[]> {
if (availableFunctionNames.length > 0) return availableFunctionNames;
try {
return await getRemoteFunctionNames();
} catch {
throw logsError;
}
}

function validateLimit(limit: string | undefined): void {
if (limit === undefined) return;
const n = Number.parseInt(limit, 10);
Expand All @@ -261,21 +270,13 @@ async function logsAction(
): Promise<RunCommandResult> {
validateLimit(options.limit);
const specifiedFunctions = parseFunctionNames(options.function);
const localProjectRoot = ctx.app?.projectRoot;

const availableFunctionNames = localProjectRoot
? await getProjectFunctionNames(localProjectRoot)
: await getRemoteFunctionNames();

const availableFunctionNames =
specifiedFunctions.length === 0 ? await getRemoteFunctionNames() : [];
const functionNames =
specifiedFunctions.length > 0 ? specifiedFunctions : availableFunctionNames;

if (functionNames.length === 0) {
return {
outroMessage: localProjectRoot
? "No functions found in this project."
: "No functions found in this app.",
};
return { outroMessage: "No functions found in this app." };
}

if (options.follow) {
Expand Down Expand Up @@ -327,7 +328,7 @@ export function getLogsCommand(): Command {
.description("Fetch function logs for this app")
.option(
"--function <names>",
"Filter by function name(s), comma-separated. If omitted, fetches logs for all project functions",
"Filter by function name(s), comma-separated. If omitted, fetches logs for all deployed functions",
)
.option(
"--since <datetime>",
Expand Down
108 changes: 78 additions & 30 deletions packages/cli/tests/cli/logs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ describe("logs command", () => {
t.expectResult(result).toContain("Something went wrong");
});

it("does not load local resources when --function is specified", async () => {
await t.givenLoggedInWithProject(fixture("invalid-entity"));
t.api.mockFunctionLogs("my-function", []);

const result = await t.run("logs", "--function", "my-function");

t.expectResult(result).toSucceed();
});

it("fetches logs for multiple functions with --function comma-separated", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockFunctionLogs("fn1", [
Expand All @@ -114,16 +123,31 @@ describe("logs command", () => {
t.expectResult(result).toContain("From fn2");
});

it("fetches logs for all project functions when no --function specified", async () => {
await t.givenLoggedInWithProject(fixture("full-project"));
t.api.mockFunctionLogs("hello", [
{ time: "2024-01-15T10:29:00Z", level: "info", message: "Hello world" },
it("fetches logs for all deployed functions inside a local project", async () => {
await t.givenLoggedInWithProject(fixture("invalid-entity"));
t.api.mockFunctionsList({
functions: [
{
name: "remote-fn",
deployment_id: "d1",
entry: "entry.ts",
files: [{ path: "entry.ts", content: "" }],
automations: [],
},
],
});
t.api.mockFunctionLogs("remote-fn", [
{
time: "2024-01-15T10:29:00Z",
level: "info",
message: "Remote function log",
},
]);

const result = await t.run("logs");

t.expectResult(result).toSucceed();
t.expectResult(result).toContain("Hello world");
t.expectResult(result).toContain("Remote function log");
});

it("fetches logs for path-named (zero-config) function", async () => {
Expand All @@ -144,17 +168,6 @@ describe("logs command", () => {

it("fetches logs for a specified function with --app-id outside a project", async () => {
await t.givenLoggedIn({ email: "test@example.com", name: "Test User" });
t.api.mockFunctionsList({
functions: [
{
name: "my-function",
deployment_id: "d1",
entry: "entry.ts",
files: [{ path: "entry.ts", content: "" }],
automations: [],
},
],
});
t.api.mockFunctionLogs("my-function", [
{
time: "2024-01-15T10:30:00.000Z",
Expand All @@ -178,17 +191,6 @@ describe("logs command", () => {
it("fetches logs for a specified function with BASE44_APP_ID outside a project", async () => {
await t.givenLoggedIn({ email: "test@example.com", name: "Test User" });
t.givenEnv({ BASE44_APP_ID: t.api.appId });
t.api.mockFunctionsList({
functions: [
{
name: "my-function",
deployment_id: "d1",
entry: "entry.ts",
files: [{ path: "entry.ts", content: "" }],
automations: [],
},
],
});
t.api.mockFunctionLogs("my-function", [
{
time: "2024-01-15T10:30:00.000Z",
Expand Down Expand Up @@ -230,13 +232,14 @@ describe("logs command", () => {
t.expectResult(result).toContain("Remote function log");
});

it("shows no functions message when project has no functions", async () => {
it("shows no functions message when the app has no deployed functions", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockFunctionsList({ functions: [] });

const result = await t.run("logs");

t.expectResult(result).toSucceed();
t.expectResult(result).toContain("No functions found in this project");
t.expectResult(result).toContain("No functions found in this app");
});

it("shows no logs message when empty", async () => {
Expand Down Expand Up @@ -311,11 +314,12 @@ describe("logs command", () => {
t.expectResult(result).toFail();
});

it("documents --level in --help", async () => {
it("documents log filters in --help", async () => {
const result = await t.run("logs", "--help");

t.expectResult(result).toSucceed();
t.expectResult(result).toContain("--level <level>");
t.expectResult(result).toContain("all deployed functions");
});

it("filters function logs by --level", async () => {
Expand Down Expand Up @@ -446,6 +450,50 @@ describe("logs command", () => {
t.expectResult(result).toContain("No Base44 app ID found");
});

it("shows deployed function names when a specified function is missing", async () => {
await t.givenLoggedInWithProject(fixture("invalid-entity"));
t.api.mockFunctionLogsError("missing-function", {
status: 404,
body: { error: "Not found" },
});
t.api.mockFunctionsList({
functions: [
{
name: "available-function",
deployment_id: "d1",
entry: "entry.ts",
files: [{ path: "entry.ts", content: "" }],
automations: [],
},
],
});

const result = await t.run("logs", "--function", "missing-function");

t.expectResult(result).toFail();
t.expectResult(result).toContain(
"Available functions in this app: available-function",
);
});

it("preserves the logs error when loading the 404 hint fails", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockFunctionLogsError("missing-function", {
status: 404,
body: { error: "Original logs error" },
});
t.api.mockFunctionsListError({
status: 500,
body: { error: "Function list error" },
});

const result = await t.run("logs", "--function", "missing-function");

t.expectResult(result).toFail();
t.expectResult(result).toContain("Original logs error");
t.expectResult(result).toNotContain("Function list error");
});

it("fails when API returns error for function logs", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockFunctionLogsError("my-function", {
Expand Down
Loading