From 5ab080bbba00efc60d9d6cc741977cc60478ed2a Mon Sep 17 00:00:00 2001 From: Tri Cao Date: Thu, 23 Oct 2025 17:03:56 -0400 Subject: [PATCH 1/6] Include additional error details in the error messages returned from orchestration API's integration --- .../provisioning/errorHandler.spec.ts | 326 ++++++++++++++++++ src/management/provisioning/errorHandler.ts | 112 ++++++ src/management/provisioning/provision.ts | 7 +- 3 files changed, 440 insertions(+), 5 deletions(-) create mode 100644 src/management/provisioning/errorHandler.spec.ts create mode 100644 src/management/provisioning/errorHandler.ts diff --git a/src/management/provisioning/errorHandler.spec.ts b/src/management/provisioning/errorHandler.spec.ts new file mode 100644 index 00000000000..34f89fc2c8d --- /dev/null +++ b/src/management/provisioning/errorHandler.spec.ts @@ -0,0 +1,326 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { FirebaseError } from "../../error"; +import { logger } from "../../logger"; +import { logProvisioningError, enhanceProvisioningError } from "./errorHandler"; + +describe("errorHandler", () => { + let sandbox: sinon.SinonSandbox; + let loggerErrorStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + loggerErrorStub = sandbox.stub(logger, "error"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("logProvisioningError", () => { + it("should not log anything for non-Error types", () => { + logProvisioningError("simple string error"); + logProvisioningError(null); + logProvisioningError(undefined); + logProvisioningError(123); + + expect(loggerErrorStub.called).to.be.false; + }); + + it("should not log anything for regular Error without context", () => { + const regularError = new Error("Regular error message"); + + logProvisioningError(regularError); + + expect(loggerErrorStub.called).to.be.false; + }); + + it("should not log anything for FirebaseError without error details", () => { + const fbError = new FirebaseError("Firebase error without details", { + context: { + body: { + error: { + code: 400, + message: "Bad Request", + status: "INVALID_ARGUMENT", + }, + }, + }, + }); + + logProvisioningError(fbError); + + expect(loggerErrorStub.called).to.be.false; + }); + + it("should log ErrorInfo details when present", () => { + const fbError = new FirebaseError("TOS required", { + context: { + body: { + error: { + code: 403, + message: "The user has not accepted the terms of service.", + status: "PERMISSION_DENIED", + details: [ + { + "@type": "type.googleapis.com/google.rpc.ErrorInfo", + reason: + "TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].", + domain: "firebase.googleapis.com", + }, + ], + }, + }, + }, + }); + + logProvisioningError(fbError); + + expect(loggerErrorStub.callCount).to.be.greaterThan(0); + expect(loggerErrorStub.calledWith("")).to.be.true; + expect(loggerErrorStub.calledWith("Error details:")).to.be.true; + expect( + loggerErrorStub.calledWith( + " Reason: TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].", + ), + ).to.be.true; + expect(loggerErrorStub.calledWith(" Domain: firebase.googleapis.com")).to.be.true; + }); + + it("should log HelpLinks when present", () => { + const fbError = new FirebaseError("TOS required", { + context: { + body: { + error: { + code: 403, + message: "The user has not accepted the terms of service.", + status: "PERMISSION_DENIED", + details: [ + { + "@type": "type.googleapis.com/google.rpc.Help", + links: [ + { + description: "Link to accept Generative Language terms of service", + url: "https://console.cloud.google.com/apis/library/generativelanguage.googleapis.com?authuser=0&forceCheckTos=true", + }, + ], + }, + ], + }, + }, + }, + }); + + logProvisioningError(fbError); + + expect(loggerErrorStub.calledWith("For help resolving this issue:")).to.be.true; + expect(loggerErrorStub.calledWith(" - Link to accept Generative Language terms of service")) + .to.be.true; + expect( + loggerErrorStub.calledWith( + " https://console.cloud.google.com/apis/library/generativelanguage.googleapis.com?authuser=0&forceCheckTos=true", + ), + ).to.be.true; + }); + + it("should log multiple ErrorInfo and HelpLinks together", () => { + const fbError = new FirebaseError("TOS required", { + context: { + body: { + error: { + code: 403, + message: "The user has not accepted the terms of service.", + status: "PERMISSION_DENIED", + details: [ + { + "@type": "type.googleapis.com/google.rpc.ErrorInfo", + reason: + "TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].", + domain: "firebase.googleapis.com", + }, + { + "@type": "type.googleapis.com/google.rpc.Help", + links: [ + { + description: "Link to accept Generative Language terms of service", + url: "https://console.cloud.google.com/apis/library/generativelanguage.googleapis.com?authuser=0&forceCheckTos=true", + }, + ], + }, + ], + }, + }, + }, + }); + + logProvisioningError(fbError); + + // Verify ErrorInfo is logged + expect( + loggerErrorStub.calledWith( + " Reason: TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].", + ), + ).to.be.true; + expect(loggerErrorStub.calledWith(" Domain: firebase.googleapis.com")).to.be.true; + + // Verify HelpLinks are logged + expect(loggerErrorStub.calledWith("For help resolving this issue:")).to.be.true; + expect(loggerErrorStub.calledWith(" - Link to accept Generative Language terms of service")) + .to.be.true; + }); + + it("should log ErrorInfo with metadata when present", () => { + const fbError = new FirebaseError("Error with metadata", { + context: { + body: { + error: { + code: 400, + message: "Invalid request", + status: "INVALID_ARGUMENT", + details: [ + { + "@type": "type.googleapis.com/google.rpc.ErrorInfo", + reason: "INVALID_FIELD", + domain: "firebase.googleapis.com", + metadata: { + field: "displayName", + constraint: "max_length", + }, + }, + ], + }, + }, + }, + }); + + logProvisioningError(fbError); + + expect(loggerErrorStub.calledWith(" Reason: INVALID_FIELD")).to.be.true; + expect(loggerErrorStub.calledWith(" Domain: firebase.googleapis.com")).to.be.true; + expect( + loggerErrorStub.calledWith( + sinon.match( + (value: string) => value.includes("Additional Info") && value.includes("field"), + ), + ), + ).to.be.true; + }); + + it("should handle multiple help links", () => { + const fbError = new FirebaseError("Multiple help links", { + context: { + body: { + error: { + code: 403, + message: "Permission denied", + status: "PERMISSION_DENIED", + details: [ + { + "@type": "type.googleapis.com/google.rpc.Help", + links: [ + { + description: "First help link", + url: "https://example.com/help1", + }, + { + description: "Second help link", + url: "https://example.com/help2", + }, + ], + }, + ], + }, + }, + }, + }); + + logProvisioningError(fbError); + + expect(loggerErrorStub.calledWith(" - First help link")).to.be.true; + expect(loggerErrorStub.calledWith(" https://example.com/help1")).to.be.true; + expect(loggerErrorStub.calledWith(" - Second help link")).to.be.true; + expect(loggerErrorStub.calledWith(" https://example.com/help2")).to.be.true; + }); + + it("should ignore unknown detail types", () => { + const fbError = new FirebaseError("Unknown detail type", { + context: { + body: { + error: { + code: 500, + message: "Internal error", + status: "INTERNAL", + details: [ + { + "@type": "type.googleapis.com/google.rpc.UnknownType", + someField: "someValue", + }, + ], + }, + }, + }, + }); + + logProvisioningError(fbError); + + // Should still log the header + expect(loggerErrorStub.calledWith("Error details:")).to.be.true; + // But should not log any specific details for unknown types + expect(loggerErrorStub.calledWith(sinon.match(/Reason/))).to.be.false; + expect(loggerErrorStub.calledWith(sinon.match(/For help/))).to.be.false; + }); + }); + + describe("enhanceProvisioningError", () => { + it("should log details and return FirebaseError with context message", () => { + const originalError = new FirebaseError("Original error", { + context: { + body: { + error: { + code: 403, + message: "Permission denied", + status: "PERMISSION_DENIED", + details: [ + { + "@type": "type.googleapis.com/google.rpc.ErrorInfo", + reason: "TOS_REQUIRED", + domain: "firebase.googleapis.com", + }, + ], + }, + }, + }, + }); + + const result = enhanceProvisioningError(originalError, "Failed to provision Firebase app"); + + // Verify logging occurred + expect(loggerErrorStub.called).to.be.true; + + // Verify returned error + expect(result).to.be.instanceOf(FirebaseError); + expect(result.message).to.equal("Failed to provision Firebase app: Original error"); + expect(result.exit).to.equal(2); + expect(result.original).to.equal(originalError); + }); + + it("should handle non-Error types gracefully", () => { + const result = enhanceProvisioningError("String error", "Operation failed"); + + expect(result).to.be.instanceOf(FirebaseError); + expect(result.message).to.equal("Operation failed: String error"); + expect(result.exit).to.equal(2); + expect(result.original).to.be.instanceOf(Error); + }); + + it("should handle regular Error without context", () => { + const regularError = new Error("Regular error"); + + const result = enhanceProvisioningError(regularError, "Context message"); + + expect(result).to.be.instanceOf(FirebaseError); + expect(result.message).to.equal("Context message: Regular error"); + expect(result.original).to.equal(regularError); + }); + }); +}); diff --git a/src/management/provisioning/errorHandler.ts b/src/management/provisioning/errorHandler.ts new file mode 100644 index 00000000000..72dbe3ecd46 --- /dev/null +++ b/src/management/provisioning/errorHandler.ts @@ -0,0 +1,112 @@ +import { FirebaseError } from "../../error"; +import { logger } from "../../logger"; + +/** + * Google RPC ErrorInfo structure + */ +interface ErrorInfo { + "@type": "type.googleapis.com/google.rpc.ErrorInfo"; + reason: string; + domain: string; + metadata?: Record; +} + +/** + * Google RPC Help structure with links + */ +interface HelpLinks { + "@type": "type.googleapis.com/google.rpc.Help"; + links: Array<{ + description: string; + url: string; + }>; +} + +/** + * Error detail can be ErrorInfo, HelpLinks, or other types + */ +type ErrorDetail = ErrorInfo | HelpLinks | Record; + +/** + * Provisioning API error structure + */ +interface ProvisioningError { + error: { + code: number; + message: string; + status: string; + details?: ErrorDetail[]; + }; +} + +/** + * Type guard for ErrorInfo + */ +function isErrorInfo(detail: ErrorDetail): detail is ErrorInfo { + return detail["@type"] === "type.googleapis.com/google.rpc.ErrorInfo"; +} + +/** + * Type guard for HelpLinks + */ +function isHelpLinks(detail: ErrorDetail): detail is HelpLinks { + return detail["@type"] === "type.googleapis.com/google.rpc.Help"; +} + +/** + * Logs detailed error information from a provisioning API error response. + * Extracts and displays error details and help links. + */ +export function logProvisioningError(err: unknown): void { + if (!(err instanceof Error)) { + return; + } + + // Check if this is a FirebaseError with context containing provisioning error + if (err instanceof FirebaseError && err.context) { + const context = err.context as { body?: ProvisioningError }; + const errorBody = context.body?.error; + + if (errorBody?.details && Array.isArray(errorBody.details)) { + logger.error(""); + logger.error("Error details:"); + + for (const detail of errorBody.details) { + if (isErrorInfo(detail)) { + logger.error(` Reason: ${detail.reason}`); + logger.error(` Domain: ${detail.domain}`); + if (detail.metadata) { + logger.error(` Additional Info: ${JSON.stringify(detail.metadata, null, 2)}`); + } + } else if (isHelpLinks(detail)) { + logger.error(""); + logger.error("For help resolving this issue:"); + for (const link of detail.links) { + logger.error(` - ${link.description}`); + logger.error(` ${link.url}`); + } + } + } + logger.error(""); + } + } +} + +/** + * Enhances an error with detailed logging from provisioning API responses. + * This function logs detailed error information and returns a user-friendly FirebaseError. + */ +export function enhanceProvisioningError( + err: unknown, + contextMessage: string, +): FirebaseError { + // Log detailed error information first + logProvisioningError(err); + + // Create and return a user-friendly error + const errorMessage = err instanceof Error ? err.message : String(err); + return new FirebaseError(`${contextMessage}: ${errorMessage}`, { + exit: 2, + original: err instanceof Error ? err : new Error(String(err)), + }); +} diff --git a/src/management/provisioning/provision.ts b/src/management/provisioning/provision.ts index 463aebe294a..3379d41eb2a 100644 --- a/src/management/provisioning/provision.ts +++ b/src/management/provisioning/provision.ts @@ -5,6 +5,7 @@ import { logger } from "../../logger"; import { pollOperation } from "../../operation-poller"; import { AppPlatform } from "../apps"; import * as types from "./types"; +import { enhanceProvisioningError } from "./errorHandler"; const apiClient = new Client({ urlPrefix: firebaseApiOrigin(), @@ -133,10 +134,6 @@ export async function provisionFirebaseApp( logger.debug("[provision] Firebase app provisioning completed successfully"); return result; } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : String(err); - throw new FirebaseError(`Failed to provision Firebase app: ${errorMessage}`, { - exit: 2, - original: err instanceof Error ? err : new Error(String(err)), - }); + throw enhanceProvisioningError(err, "Failed to provision Firebase app"); } } From c37c8a3a09c8b898a9567ce37951fdb5b0c7cba4 Mon Sep 17 00:00:00 2001 From: Tri Cao Date: Thu, 23 Oct 2025 17:20:35 -0400 Subject: [PATCH 2/6] fix linting --- src/management/provisioning/errorHandler.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/management/provisioning/errorHandler.ts b/src/management/provisioning/errorHandler.ts index 72dbe3ecd46..8c4cc3f9031 100644 --- a/src/management/provisioning/errorHandler.ts +++ b/src/management/provisioning/errorHandler.ts @@ -96,10 +96,7 @@ export function logProvisioningError(err: unknown): void { * Enhances an error with detailed logging from provisioning API responses. * This function logs detailed error information and returns a user-friendly FirebaseError. */ -export function enhanceProvisioningError( - err: unknown, - contextMessage: string, -): FirebaseError { +export function enhanceProvisioningError(err: unknown, contextMessage: string): FirebaseError { // Log detailed error information first logProvisioningError(err); From c164e201dd8084e63c2ba46b88e700db8a890e03 Mon Sep 17 00:00:00 2001 From: Tri Cao Date: Thu, 23 Oct 2025 17:30:06 -0400 Subject: [PATCH 3/6] Incoporate Gemini review --- src/management/provisioning/errorHandler.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/management/provisioning/errorHandler.ts b/src/management/provisioning/errorHandler.ts index 8c4cc3f9031..f3bae01ec08 100644 --- a/src/management/provisioning/errorHandler.ts +++ b/src/management/provisioning/errorHandler.ts @@ -1,4 +1,4 @@ -import { FirebaseError } from "../../error"; +import { FirebaseError, getError } from "../../error"; import { logger } from "../../logger"; /** @@ -76,7 +76,7 @@ export function logProvisioningError(err: unknown): void { logger.error(` Reason: ${detail.reason}`); logger.error(` Domain: ${detail.domain}`); if (detail.metadata) { - logger.error(` Additional Info: ${JSON.stringify(detail.metadata, null, 2)}`); + logger.error(` Additional Info: ${JSON.stringify(detail.metadata)}`); } } else if (isHelpLinks(detail)) { logger.error(""); @@ -101,9 +101,9 @@ export function enhanceProvisioningError(err: unknown, contextMessage: string): logProvisioningError(err); // Create and return a user-friendly error - const errorMessage = err instanceof Error ? err.message : String(err); - return new FirebaseError(`${contextMessage}: ${errorMessage}`, { + const originalError = getError(err); + return new FirebaseError(`${contextMessage}: ${originalError.message}`, { exit: 2, - original: err instanceof Error ? err : new Error(String(err)), + original: originalError, }); } From 458aa716d0a17b51aea233eb5c98f8f47160f0d4 Mon Sep 17 00:00:00 2001 From: Tri Cao Date: Thu, 23 Oct 2025 17:50:53 -0400 Subject: [PATCH 4/6] Include error details in error message instead of logging separately --- src/init/features/ailogic/index.ts | 6 + .../provisioning/errorHandler.spec.ts | 237 ++++++------------ src/management/provisioning/errorHandler.ts | 47 ++-- 3 files changed, 112 insertions(+), 178 deletions(-) diff --git a/src/init/features/ailogic/index.ts b/src/init/features/ailogic/index.ts index 9001d42e06e..aa5ddc00136 100644 --- a/src/init/features/ailogic/index.ts +++ b/src/init/features/ailogic/index.ts @@ -142,6 +142,12 @@ export async function actuate(setup: Setup): Promise { "Place this config file in the appropriate location for your platform.", ); } catch (error) { + if (error instanceof FirebaseError) { + throw new FirebaseError(`AI Logic setup failed: ${error.message}`, { + original: error.original || error, + exit: error.exit, + }); + } throw new FirebaseError( `AI Logic setup failed: ${error instanceof Error ? error.message : String(error)}`, { original: error instanceof Error ? error : new Error(String(error)), exit: 2 }, diff --git a/src/management/provisioning/errorHandler.spec.ts b/src/management/provisioning/errorHandler.spec.ts index 34f89fc2c8d..ecd6784984d 100644 --- a/src/management/provisioning/errorHandler.spec.ts +++ b/src/management/provisioning/errorHandler.spec.ts @@ -1,60 +1,11 @@ import { expect } from "chai"; -import * as sinon from "sinon"; import { FirebaseError } from "../../error"; -import { logger } from "../../logger"; -import { logProvisioningError, enhanceProvisioningError } from "./errorHandler"; +import { enhanceProvisioningError } from "./errorHandler"; describe("errorHandler", () => { - let sandbox: sinon.SinonSandbox; - let loggerErrorStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - loggerErrorStub = sandbox.stub(logger, "error"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("logProvisioningError", () => { - it("should not log anything for non-Error types", () => { - logProvisioningError("simple string error"); - logProvisioningError(null); - logProvisioningError(undefined); - logProvisioningError(123); - - expect(loggerErrorStub.called).to.be.false; - }); - - it("should not log anything for regular Error without context", () => { - const regularError = new Error("Regular error message"); - - logProvisioningError(regularError); - - expect(loggerErrorStub.called).to.be.false; - }); - - it("should not log anything for FirebaseError without error details", () => { - const fbError = new FirebaseError("Firebase error without details", { - context: { - body: { - error: { - code: 400, - message: "Bad Request", - status: "INVALID_ARGUMENT", - }, - }, - }, - }); - - logProvisioningError(fbError); - - expect(loggerErrorStub.called).to.be.false; - }); - - it("should log ErrorInfo details when present", () => { - const fbError = new FirebaseError("TOS required", { + describe("enhanceProvisioningError", () => { + it("should include ErrorInfo details in error message", () => { + const originalError = new FirebaseError("Permission denied", { context: { body: { error: { @@ -74,21 +25,21 @@ describe("errorHandler", () => { }, }); - logProvisioningError(fbError); + const result = enhanceProvisioningError(originalError, "Failed to provision Firebase app"); - expect(loggerErrorStub.callCount).to.be.greaterThan(0); - expect(loggerErrorStub.calledWith("")).to.be.true; - expect(loggerErrorStub.calledWith("Error details:")).to.be.true; - expect( - loggerErrorStub.calledWith( - " Reason: TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].", - ), - ).to.be.true; - expect(loggerErrorStub.calledWith(" Domain: firebase.googleapis.com")).to.be.true; + expect(result).to.be.instanceOf(FirebaseError); + expect(result.message).to.include("Failed to provision Firebase app: Permission denied"); + expect(result.message).to.include("Error details:"); + expect(result.message).to.include( + "Reason: TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].", + ); + expect(result.message).to.include("Domain: firebase.googleapis.com"); + expect(result.exit).to.equal(2); + expect(result.original).to.equal(originalError); }); - it("should log HelpLinks when present", () => { - const fbError = new FirebaseError("TOS required", { + it("should include HelpLinks in error message", () => { + const originalError = new FirebaseError("Permission denied", { context: { body: { error: { @@ -111,20 +62,17 @@ describe("errorHandler", () => { }, }); - logProvisioningError(fbError); + const result = enhanceProvisioningError(originalError, "Failed to provision Firebase app"); - expect(loggerErrorStub.calledWith("For help resolving this issue:")).to.be.true; - expect(loggerErrorStub.calledWith(" - Link to accept Generative Language terms of service")) - .to.be.true; - expect( - loggerErrorStub.calledWith( - " https://console.cloud.google.com/apis/library/generativelanguage.googleapis.com?authuser=0&forceCheckTos=true", - ), - ).to.be.true; + expect(result.message).to.include("For help resolving this issue:"); + expect(result.message).to.include("Link to accept Generative Language terms of service"); + expect(result.message).to.include( + "https://console.cloud.google.com/apis/library/generativelanguage.googleapis.com?authuser=0&forceCheckTos=true", + ); }); - it("should log multiple ErrorInfo and HelpLinks together", () => { - const fbError = new FirebaseError("TOS required", { + it("should include both ErrorInfo and HelpLinks in error message", () => { + const originalError = new FirebaseError("Permission denied", { context: { body: { error: { @@ -153,24 +101,22 @@ describe("errorHandler", () => { }, }); - logProvisioningError(fbError); + const result = enhanceProvisioningError(originalError, "Failed to provision Firebase app"); - // Verify ErrorInfo is logged - expect( - loggerErrorStub.calledWith( - " Reason: TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].", - ), - ).to.be.true; - expect(loggerErrorStub.calledWith(" Domain: firebase.googleapis.com")).to.be.true; + // Verify ErrorInfo is included + expect(result.message).to.include("Error details:"); + expect(result.message).to.include( + "Reason: TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].", + ); + expect(result.message).to.include("Domain: firebase.googleapis.com"); - // Verify HelpLinks are logged - expect(loggerErrorStub.calledWith("For help resolving this issue:")).to.be.true; - expect(loggerErrorStub.calledWith(" - Link to accept Generative Language terms of service")) - .to.be.true; + // Verify HelpLinks are included + expect(result.message).to.include("For help resolving this issue:"); + expect(result.message).to.include("Link to accept Generative Language terms of service"); }); - it("should log ErrorInfo with metadata when present", () => { - const fbError = new FirebaseError("Error with metadata", { + it("should include ErrorInfo with metadata in error message", () => { + const originalError = new FirebaseError("Invalid request", { context: { body: { error: { @@ -193,21 +139,16 @@ describe("errorHandler", () => { }, }); - logProvisioningError(fbError); + const result = enhanceProvisioningError(originalError, "Operation failed"); - expect(loggerErrorStub.calledWith(" Reason: INVALID_FIELD")).to.be.true; - expect(loggerErrorStub.calledWith(" Domain: firebase.googleapis.com")).to.be.true; - expect( - loggerErrorStub.calledWith( - sinon.match( - (value: string) => value.includes("Additional Info") && value.includes("field"), - ), - ), - ).to.be.true; + expect(result.message).to.include("Reason: INVALID_FIELD"); + expect(result.message).to.include("Domain: firebase.googleapis.com"); + expect(result.message).to.include("Additional Info:"); + expect(result.message).to.include("field"); }); - it("should handle multiple help links", () => { - const fbError = new FirebaseError("Multiple help links", { + it("should include multiple help links in error message", () => { + const originalError = new FirebaseError("Multiple help links", { context: { body: { error: { @@ -234,74 +175,32 @@ describe("errorHandler", () => { }, }); - logProvisioningError(fbError); - - expect(loggerErrorStub.calledWith(" - First help link")).to.be.true; - expect(loggerErrorStub.calledWith(" https://example.com/help1")).to.be.true; - expect(loggerErrorStub.calledWith(" - Second help link")).to.be.true; - expect(loggerErrorStub.calledWith(" https://example.com/help2")).to.be.true; - }); - - it("should ignore unknown detail types", () => { - const fbError = new FirebaseError("Unknown detail type", { - context: { - body: { - error: { - code: 500, - message: "Internal error", - status: "INTERNAL", - details: [ - { - "@type": "type.googleapis.com/google.rpc.UnknownType", - someField: "someValue", - }, - ], - }, - }, - }, - }); - - logProvisioningError(fbError); + const result = enhanceProvisioningError(originalError, "Operation failed"); - // Should still log the header - expect(loggerErrorStub.calledWith("Error details:")).to.be.true; - // But should not log any specific details for unknown types - expect(loggerErrorStub.calledWith(sinon.match(/Reason/))).to.be.false; - expect(loggerErrorStub.calledWith(sinon.match(/For help/))).to.be.false; + expect(result.message).to.include("First help link"); + expect(result.message).to.include("https://example.com/help1"); + expect(result.message).to.include("Second help link"); + expect(result.message).to.include("https://example.com/help2"); }); - }); - describe("enhanceProvisioningError", () => { - it("should log details and return FirebaseError with context message", () => { - const originalError = new FirebaseError("Original error", { + it("should handle errors without details gracefully", () => { + const originalError = new FirebaseError("Firebase error without details", { context: { body: { error: { - code: 403, - message: "Permission denied", - status: "PERMISSION_DENIED", - details: [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - reason: "TOS_REQUIRED", - domain: "firebase.googleapis.com", - }, - ], + code: 400, + message: "Bad Request", + status: "INVALID_ARGUMENT", }, }, }, }); - const result = enhanceProvisioningError(originalError, "Failed to provision Firebase app"); - - // Verify logging occurred - expect(loggerErrorStub.called).to.be.true; + const result = enhanceProvisioningError(originalError, "Operation failed"); - // Verify returned error expect(result).to.be.instanceOf(FirebaseError); - expect(result.message).to.equal("Failed to provision Firebase app: Original error"); + expect(result.message).to.equal("Operation failed: Firebase error without details"); expect(result.exit).to.equal(2); - expect(result.original).to.equal(originalError); }); it("should handle non-Error types gracefully", () => { @@ -322,5 +221,31 @@ describe("errorHandler", () => { expect(result.message).to.equal("Context message: Regular error"); expect(result.original).to.equal(regularError); }); + + it("should ignore unknown detail types", () => { + const originalError = new FirebaseError("Unknown detail type", { + context: { + body: { + error: { + code: 500, + message: "Internal error", + status: "INTERNAL", + details: [ + { + "@type": "type.googleapis.com/google.rpc.UnknownType", + someField: "someValue", + }, + ], + }, + }, + }, + }); + + const result = enhanceProvisioningError(originalError, "Operation failed"); + + expect(result.message).to.equal("Operation failed: Unknown detail type"); + expect(result.message).to.not.include("Error details:"); + expect(result.message).to.not.include("For help"); + }); }); }); diff --git a/src/management/provisioning/errorHandler.ts b/src/management/provisioning/errorHandler.ts index f3bae01ec08..e60da66bc99 100644 --- a/src/management/provisioning/errorHandler.ts +++ b/src/management/provisioning/errorHandler.ts @@ -1,5 +1,4 @@ import { FirebaseError, getError } from "../../error"; -import { logger } from "../../logger"; /** * Google RPC ErrorInfo structure @@ -54,12 +53,12 @@ function isHelpLinks(detail: ErrorDetail): detail is HelpLinks { } /** - * Logs detailed error information from a provisioning API error response. - * Extracts and displays error details and help links. + * Extracts detailed error information from a provisioning API error response. + * Returns a formatted string with error details and help links. */ -export function logProvisioningError(err: unknown): void { +function extractErrorDetails(err: unknown): string { if (!(err instanceof Error)) { - return; + return ""; } // Check if this is a FirebaseError with context containing provisioning error @@ -68,41 +67,45 @@ export function logProvisioningError(err: unknown): void { const errorBody = context.body?.error; if (errorBody?.details && Array.isArray(errorBody.details)) { - logger.error(""); - logger.error("Error details:"); + const parts: string[] = []; for (const detail of errorBody.details) { if (isErrorInfo(detail)) { - logger.error(` Reason: ${detail.reason}`); - logger.error(` Domain: ${detail.domain}`); + parts.push(`Error details:`); + parts.push(` Reason: ${detail.reason}`); + parts.push(` Domain: ${detail.domain}`); if (detail.metadata) { - logger.error(` Additional Info: ${JSON.stringify(detail.metadata)}`); + parts.push(` Additional Info: ${JSON.stringify(detail.metadata)}`); } } else if (isHelpLinks(detail)) { - logger.error(""); - logger.error("For help resolving this issue:"); + parts.push(`\nFor help resolving this issue:`); for (const link of detail.links) { - logger.error(` - ${link.description}`); - logger.error(` ${link.url}`); + parts.push(` - ${link.description}`); + parts.push(` ${link.url}`); } } } - logger.error(""); + + return parts.length > 0 ? `\n\n${parts.join("\n")}` : ""; } } + + return ""; } /** - * Enhances an error with detailed logging from provisioning API responses. - * This function logs detailed error information and returns a user-friendly FirebaseError. + * Enhances an error with detailed information from provisioning API responses. + * This function extracts error details and includes them in the error message. */ export function enhanceProvisioningError(err: unknown, contextMessage: string): FirebaseError { - // Log detailed error information first - logProvisioningError(err); - - // Create and return a user-friendly error const originalError = getError(err); - return new FirebaseError(`${contextMessage}: ${originalError.message}`, { + const errorDetails = extractErrorDetails(err); + + const fullMessage = errorDetails + ? `${contextMessage}: ${originalError.message}${errorDetails}` + : `${contextMessage}: ${originalError.message}`; + + return new FirebaseError(fullMessage, { exit: 2, original: originalError, }); From 3688d0df349d33c15100db5156e045bc7463509c Mon Sep 17 00:00:00 2001 From: Tri Cao Date: Thu, 23 Oct 2025 17:53:54 -0400 Subject: [PATCH 5/6] Remove unnecessary error rewrapping in ailogic index --- src/init/features/ailogic/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/init/features/ailogic/index.ts b/src/init/features/ailogic/index.ts index aa5ddc00136..9001d42e06e 100644 --- a/src/init/features/ailogic/index.ts +++ b/src/init/features/ailogic/index.ts @@ -142,12 +142,6 @@ export async function actuate(setup: Setup): Promise { "Place this config file in the appropriate location for your platform.", ); } catch (error) { - if (error instanceof FirebaseError) { - throw new FirebaseError(`AI Logic setup failed: ${error.message}`, { - original: error.original || error, - exit: error.exit, - }); - } throw new FirebaseError( `AI Logic setup failed: ${error instanceof Error ? error.message : String(error)}`, { original: error instanceof Error ? error : new Error(String(error)), exit: 2 }, From 9d5e5fbe835738101fad009ec7289ca56771a5c3 Mon Sep 17 00:00:00 2001 From: Tri Cao Date: Mon, 27 Oct 2025 15:14:45 -0400 Subject: [PATCH 6/6] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 748659b90a7..5ee3d69a119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Added `functions.list_functions` as a MCP tool (#9369) - Added AI Logic to `firebase init` CLI command and `firebase_init` MCP tool. (#9185) +- Improved error messages for Firebase AI Logic provisioning during 'firebase init' (#9377)