Skip to content
Draft
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 common/api-review/app.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export const _DEFAULT_ENTRY_NAME = "[DEFAULT]";
// @public
export function deleteApp(app: FirebaseApp): Promise<void>;

// @public (undocumented)
export function enableContextualErrors(enabled: boolean): void;

// @public
export interface FirebaseApp {
automaticDataCollectionEnabled: boolean;
Expand Down
17 changes: 16 additions & 1 deletion common/api-review/firestore-lite.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ export function connectFirestoreEmulator(firestore: Firestore, host: string, por
// @public
export function count(): AggregateField<number>;

// @public (undocumented)
export type CustomErrorInfo = WithPath;

// @public
export function deleteDoc<AppModelType, DbModelType extends DocumentData>(reference: DocumentReference<AppModelType, DbModelType>): Promise<void>;

Expand Down Expand Up @@ -200,7 +203,8 @@ export interface FirestoreDataConverter<AppModelType, DbModelType extends Docume
// @public
export class FirestoreError extends FirebaseError {
readonly code: FirestoreErrorCode;
readonly message: string;
// (undocumented)
copyWithAuthInfo(idToken: string | null): FirestoreError;
readonly stack?: string;
}

Expand Down Expand Up @@ -269,6 +273,9 @@ export type NestedUpdateFields<T extends Record<string, unknown>> = UnionToInter
[K in keyof T & string]: ChildUpdateFields<K, T[K]>;
}[keyof T & string]>;

// @public (undocumented)
export type OperationType = 'read' | 'write' | 'listen';

// @public
export function or(...queryConstraints: QueryFilterConstraint[]): QueryCompositeFilterConstraint;

Expand Down Expand Up @@ -492,6 +499,14 @@ export type WithFieldValue<T> = T | (T extends Primitive ? T : T extends {} ? {
[K in keyof T]: WithFieldValue<T[K]> | FieldValue;
} : never);

// @public (undocumented)
export interface WithPath {
// (undocumented)
operationType: OperationType;
// (undocumented)
path: string;
}

// @public
export class WriteBatch {
commit(): Promise<void>;
Expand Down
17 changes: 16 additions & 1 deletion common/api-review/firestore.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ export function connectFirestoreEmulator(firestore: Firestore, host: string, por
// @public
export function count(): AggregateField<number>;

// @public (undocumented)
export type CustomErrorInfo = WithPath;

// @public
export function deleteAllPersistentCacheIndexes(indexManager: PersistentCacheIndexManager): void;

Expand Down Expand Up @@ -251,7 +254,8 @@ export interface FirestoreDataConverter<AppModelType, DbModelType extends Docume
// @public
export class FirestoreError extends FirebaseError {
readonly code: FirestoreErrorCode;
readonly message: string;
// (undocumented)
copyWithAuthInfo(idToken: string | null): FirestoreError;
readonly stack?: string;
}

Expand Down Expand Up @@ -523,6 +527,9 @@ export function onSnapshotsInSync(firestore: Firestore, observer: {
// @public
export function onSnapshotsInSync(firestore: Firestore, onSync: () => void): Unsubscribe;

// @public (undocumented)
export type OperationType = 'read' | 'write' | 'listen';

// @public
export function or(...queryConstraints: QueryFilterConstraint[]): QueryCompositeFilterConstraint;

Expand Down Expand Up @@ -834,6 +841,14 @@ export type WithFieldValue<T> = T | (T extends Primitive ? T : T extends {} ? {
[K in keyof T]: WithFieldValue<T[K]> | FieldValue;
} : never);

// @public (undocumented)
export interface WithPath {
// (undocumented)
operationType: OperationType;
// (undocumented)
path: string;
}

// @public
export class WriteBatch {
commit(): Promise<void>;
Expand Down
50 changes: 47 additions & 3 deletions common/api-review/util.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export const assertionError: (message: string) => Error;
// @public
export function async(fn: Function, onError?: ErrorFn): Function;

// Warning: (ae-missing-release-tag) "AuthInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface AuthInfo {
// (undocumented)
authInfo: ErrorAuthInfo | null;
}

// Warning: (ae-forgotten-export) The symbol "Base64" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "base64" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
Expand Down Expand Up @@ -140,6 +148,25 @@ export type EmulatorMockTokenOptions = ({
sub: string;
}) & Partial<FirebaseIdToken>;

// Warning: (ae-missing-release-tag) "enableContextualErrors" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function enableContextualErrors(enabled: boolean): void;

// Warning: (ae-missing-release-tag) "ErrorAuthInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ErrorAuthInfo {
// (undocumented)
email: string | null;
// (undocumented)
emailVerified: boolean;
// (undocumented)
isAnonymous: boolean;
// (undocumented)
userId: string;
}

// Warning: (ae-missing-release-tag) "ErrorData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down Expand Up @@ -207,12 +234,12 @@ export interface FirebaseDefaults {
// Warning: (ae-missing-release-tag) "FirebaseError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class FirebaseError extends Error {
export class FirebaseError<T = Record<string, unknown>> extends Error {
constructor(
code: string, message: string,
customData?: Record<string, unknown> | undefined);
customData?: T | undefined);
readonly code: string;
customData?: Record<string, unknown> | undefined;
customData?: T | undefined;
readonly name: string;
}

Expand All @@ -224,6 +251,13 @@ export type FirebaseSignInProvider = 'custom' | 'email' | 'password' | 'phone' |
// @public
export function generateSHA256Hash(input: string): Promise<string>;

// Warning: (ae-missing-release-tag) "getContextualMsg" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function getContextualMsg<T extends {
authInfo: ErrorAuthInfo | null;
}, E extends FirebaseError<T>>(originalError: E): string;

// @public
export const getDefaultAppConfig: () => Record<string, string> | undefined;

Expand Down Expand Up @@ -275,6 +309,11 @@ export function isCloudflareWorker(): boolean;
// @public
export function isCloudWorkstation(url: string): boolean;

// Warning: (ae-missing-release-tag) "isContextualErrorsEnabled" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function isContextualErrorsEnabled(): boolean;

// Warning: (ae-missing-release-tag) "isElectron" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
Expand Down Expand Up @@ -401,6 +440,11 @@ export interface Observer<T> {
// @public
export function ordinal(i: number): string;

// Warning: (ae-missing-release-tag) "parseIdTokenToAuthInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function parseIdTokenToAuthInfo(idToken: string): ErrorAuthInfo;

// Warning: (ae-missing-release-tag) "PartialObserver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down
118 changes: 118 additions & 0 deletions common/skills/add-contextual-errors/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
name: add-contextual-errors
description: Enrich errors thrown by Firebase SDK packages with operational context (operation name, variables, resource paths, auth state) to improve debuggability, while respecting the central detailedErrors toggle.
license: Apache-2.0
metadata:
author: Firebase
version: "1.0"
---

# Skill: Add Contextual Errors to Firebase SDK Packages

## Goal
Enrich errors thrown by Firebase SDK packages with operational context (operation name, variables, resource paths, auth state) to improve debuggability, while respecting the central `detailedErrors` toggle to prevent information leakage in production.

## Applicability
This pattern applies to any package in the Firebase JS SDK that performs network operations (e.g., `firestore`, `functions`, `storage`, `data-connect`).

## Core Principles

1. **Capture Auth Context**: Always capture the auth state (e.g., idToken) to understand who made the request.
2. **No `any`**: Ensure all types are properly defined. Never use `any` to bypass type checks.
3. **Capture at Request Time**: Capture the context (especially auth state) at the exact moment the network request is initiated, not when the high-level API function is called.
4. **Respect `detailedErrors`**: Use `throwDetailedError` from `@firebase/util` to ensure context is only serialized into the error message if the user has enabled detailed errors via `setDetailedErrors`.
5. **Preserve Custom Error Types**: Ensure that the thrown error remains an instance of the specific package's error class (e.g., `DataConnectError`, `FirestoreError`) so that `instanceof` checks still work for developers.
6. **Respect current API surface and do not modify it**: Ensure that all user-facing APIs are not affected. Usually this is listed in an `api.ts` file or using api-extractor.

---

## Steps to Apply

### Step 1: Define the Operation Context

Define an interface in the package to represent the context of an operation. This should include whatever client-side state is useful for debugging.

```typescript
export interface OperationContext {
operationName?: string;
variables?: Record<string, unknown>;
// Add other package-specific context here (e.g. storage bucket, firestore path)
auth: { uid?: string };
}
```

### Step 2: Update Custom Error Classes

Update the package's custom error class to optionally store this context. Ensure it is mutable or can be set in the constructor.

```typescript
export class MyPackageError extends FirebaseError {
context?: OperationContext;

constructor(code: string, message: string, context?: OperationContext) {
super(code, message);
this.context = context;
Object.setPrototypeOf(this, MyPackageError.prototype);
}
}
```

### Step 3: Identify Execution Points

Find the low-level methods in the package that actually send requests to the backend (e.g., in the transport layer or RPC invoker).

### Step 4: Implement Request-Time Capture and Enrichment

In the identified execution points, wrap the network call in a try/catch block and apply the enrichment workflow.

```typescript
// Inside a low-level network operation method
try {
return await sendNetworkRequest(...);
} catch (error) {
if (error instanceof MyPackageError) {
// 1. Capture context at the exact time of request
const context: OperationContext = {
operationName: 'myOperation',
variables: requestBody as Record<string, unknown>,
auth: { uid: this.authProvider?.getCurrentUid() } // Capture current state
};

// 2. Attach context to customData for throwDetailedError
const customData = {
...error.customData,
context
};

// 3. Parse token and create temporary FirebaseErrorWithAuthInfo
const authInfo = this._accessToken ? parseIdTokenToAuthInfo(this._accessToken) : null;
const errorWithAuthInfo = new FirebaseErrorWithAuthInfo(
new FirebaseError(error.code, error.message, customData),
authInfo
);

// 4. Call throwDetailedError to respect the central toggle
const detailedErr = throwDetailedError(this.authProvider.app, errorWithAuthInfo);

// 5. Preserving Custom Error instance while using the detailed message
const finalErr = new MyPackageError(error.code, detailedErr.message);

// 6. Optionally attach structured context if detailed errors are enabled
if (detailedErr instanceof DetailedFirebaseError) {
finalErr.context = context;
}

throw finalErr;
}
throw error;
}
```

---

## Verification Plan

### Automated Tests
- Verify that errors thrown when `setDetailedErrors(app, false)` do NOT contain operation variables or decoded id token in the message.
- Verify that errors thrown when `setDetailedErrors(app, true)` DO contain operation variables and decoded id token in the message.
- Verify that `error instanceof MyPackageError` remains true in both cases.
1 change: 1 addition & 0 deletions config/tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"incremental": true,
"sourceMap": true,
"target": "es2020",
"typeRoots": [
Expand Down
4 changes: 4 additions & 0 deletions docs-devsite/_toc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,8 @@ toc:
path: /docs/reference/js/firestore_.unsubscribe.md
- title: VectorValue
path: /docs/reference/js/firestore_.vectorvalue.md
- title: WithPath
path: /docs/reference/js/firestore_.withpath.md
- title: WriteBatch
path: /docs/reference/js/firestore_.writebatch.md
- title: firestore/lite
Expand Down Expand Up @@ -599,6 +601,8 @@ toc:
path: /docs/reference/js/firestore_lite.transactionoptions.md
- title: VectorValue
path: /docs/reference/js/firestore_lite.vectorvalue.md
- title: WithPath
path: /docs/reference/js/firestore_lite.withpath.md
- title: WriteBatch
path: /docs/reference/js/firestore_lite.writebatch.md
- title: firestore/lite/pipelines
Expand Down
22 changes: 22 additions & 0 deletions docs-devsite/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ This package coordinates the communication between the different Firebase compon
| [initializeApp()](./app.md#initializeapp) | Creates and initializes a FirebaseApp instance. |
| <b>function(config, ...)</b> |
| [initializeServerApp(config)](./app.md#initializeserverapp_e7d0728) | Creates and initializes a [FirebaseServerApp](./app.firebaseserverapp.md#firebaseserverapp_interface) instance. |
| <b>function(enabled, ...)</b> |
| [enableContextualErrors(enabled)](./app.md#enablecontextualerrors_25adbe2) | |
| <b>function(libraryKeyOrName, ...)</b> |
| [registerVersion(libraryKeyOrName, version, variant)](./app.md#registerversion_f673248) | Registers a library's name and version for platform logging purposes. |
| <b>function(logCallback, ...)</b> |
Expand Down Expand Up @@ -150,6 +152,26 @@ If [FirebaseServerAppSettings.releaseOnDeref](./app.firebaseserverappsettings.md

If the `FIREBASE_OPTIONS` environment variable does not contain a valid project configuration required for auto-initialization.

## function(enabled, ...)

### enableContextualErrors(enabled) {:#enablecontextualerrors_25adbe2}

<b>Signature:</b>

```typescript
export declare function enableContextualErrors(enabled: boolean): void;
```

#### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| enabled | boolean | |

<b>Returns:</b>

void

## function(libraryKeyOrName, ...)

### registerVersion(libraryKeyOrName, version, variant) {:#registerversion_f673248}
Expand Down
Loading
Loading