diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts
index a2feb19507f..3225d826126 100644
--- a/packages/firestore/src/api/database.ts
+++ b/packages/firestore/src/api/database.ts
@@ -269,6 +269,7 @@ export function getFirestore(
connectFirestoreEmulator(db, ...emulator);
}
}
+
return db;
}
diff --git a/packages/firestore/src/local/simple_db.ts b/packages/firestore/src/local/simple_db.ts
index 1e315c5dae6..81dea4af619 100644
--- a/packages/firestore/src/local/simple_db.ts
+++ b/packages/firestore/src/local/simple_db.ts
@@ -17,7 +17,7 @@
import { getGlobal, getUA, isIndexedDBAvailable } from '@firebase/util';
-import { debugAssert } from '../util/assert';
+import { debugAssert, fail } from '../util/assert';
import { Code, FirestoreError } from '../util/error';
import { logDebug, logError, logWarn } from '../util/log';
import { Deferred } from '../util/promise';
@@ -355,7 +355,8 @@ export class SimpleDb {
) {
// This thrown error will get passed to the `onerror` callback
// registered above, and will then be propagated correctly.
- throw new Error(
+ fail(
+ 0x3456,
`refusing to open IndexedDB database due to potential ` +
`corruption of the IndexedDB database data; this corruption ` +
`could be caused by clicking the "clear site data" button in ` +
diff --git a/packages/firestore/src/util/log.ts b/packages/firestore/src/util/log.ts
index cbdbad38ecf..8a778dd0822 100644
--- a/packages/firestore/src/util/log.ts
+++ b/packages/firestore/src/util/log.ts
@@ -15,14 +15,18 @@
* limitations under the License.
*/
-import { Logger, LogLevel, LogLevelString } from '@firebase/logger';
+import { Logger, LogHandler, LogLevel, LogLevelString } from '@firebase/logger';
import { SDK_VERSION } from '../core/version';
import { formatJSON } from '../platform/format_json';
+import { generateUniqueDebugId } from './debug_uid';
+
export { LogLevel, LogLevelString };
const logClient = new Logger('@firebase/firestore');
+const defaultLogHandler = logClient.logHandler;
+let logBuffer: LogBuffer | undefined;
// Helper methods are needed because variables can't be exported as read/write
export function getLogLevel(): LogLevel {
@@ -41,20 +45,37 @@ export function getLogLevel(): LogLevel {
*
`error` to log errors only.
* `silent` to turn off logging.
*
+ * @param includeContext - If set to a positive value, the logger will buffer
+ * all log messages (of all log levels) and log the most recent messages
+ * when a message of `logLevel` is seen. This is useful if you want to get
+ * debug logging from the SDK leading up to a warning or error, but do not
+ * always want debug log verbosity. This param specifies how many messages
+ * to buffer.
*/
-export function setLogLevel(logLevel: LogLevelString): void {
+export function setLogLevel(
+ logLevel: LogLevelString,
+ includeContext: number = 0
+): void {
logClient.setLogLevel(logLevel);
+
+ if (includeContext > 0) {
+ logBuffer = new LogBuffer(includeContext);
+ logClient.logHandler = bufferingLogHandler;
+ } else {
+ logBuffer = undefined;
+ logClient.logHandler = defaultLogHandler;
+ }
}
export function logDebug(msg: string, ...obj: unknown[]): void {
- if (logClient.logLevel <= LogLevel.DEBUG) {
+ if (logBuffer || logClient.logLevel <= LogLevel.DEBUG) {
const args = obj.map(argToString);
logClient.debug(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
}
}
export function logError(msg: string, ...obj: unknown[]): void {
- if (logClient.logLevel <= LogLevel.ERROR) {
+ if (logBuffer || logClient.logLevel <= LogLevel.ERROR) {
const args = obj.map(argToString);
logClient.error(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
}
@@ -64,7 +85,7 @@ export function logError(msg: string, ...obj: unknown[]): void {
* @internal
*/
export function logWarn(msg: string, ...obj: unknown[]): void {
- if (logClient.logLevel <= LogLevel.WARN) {
+ if (logBuffer || logClient.logLevel <= LogLevel.WARN) {
const args = obj.map(argToString);
logClient.warn(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
}
@@ -85,3 +106,176 @@ function argToString(obj: unknown): string | unknown {
}
}
}
+
+class LogBuffer {
+ private _buffer: Array<{ level: LogLevel; now: string; args: unknown[] }>;
+ private _numTruncated: number = 0;
+
+ constructor(readonly bufferSize: number) {
+ this._buffer = [];
+ this._numTruncated = 0;
+ }
+
+ /**
+ * Clear the log buffer
+ */
+ clear(): void {
+ this._buffer = [];
+ this._numTruncated = 0;
+ }
+
+ /**
+ * Add a new log message to the buffer. If the buffer will exceed
+ * the allocated buffer size, then remove the oldest message from
+ * the buffer.
+ * @param level
+ * @param now
+ * @param args
+ */
+ add(level: LogLevel, now: string, args: unknown[]): void {
+ this._buffer.push({
+ level,
+ now,
+ args
+ });
+
+ if (this._buffer.length > this.bufferSize) {
+ // remove the first (oldest) element
+ this._buffer.shift();
+ this._numTruncated++;
+ }
+ }
+
+ /**
+ * Returns the number of old log messages that have been
+ * truncated from the log to maintain buffer size.
+ */
+ get numTruncated(): number {
+ return this._numTruncated;
+ }
+
+ get first(): { level: LogLevel; now: string; args: unknown[] } | undefined {
+ return this._buffer[0];
+ }
+
+ /**
+ * Iterate from oldest to newest.
+ */
+ [Symbol.iterator](): Iterator<{
+ level: LogLevel;
+ now: string;
+ args: unknown[];
+ }> {
+ let currentIndex = 0;
+ // Create a snapshot of the buffer for iteration.
+ // This ensures that if the buffer is modified while iterating (e.g., by adding new logs),
+ // the iterator will continue to iterate over the state of the buffer as it was when iteration began.
+ // It also means you iterate from the oldest to the newest log.
+ const bufferSnapshot = [...this._buffer];
+
+ return {
+ next: (): IteratorResult<{
+ level: LogLevel;
+ now: string;
+ args: unknown[];
+ }> => {
+ if (currentIndex < bufferSnapshot.length) {
+ return { value: bufferSnapshot[currentIndex++], done: false };
+ } else {
+ return { value: undefined, done: true };
+ }
+ }
+ };
+ }
+}
+
+/**
+ * By default, `console.debug` is not displayed in the developer console (in
+ * chrome). To avoid forcing users to have to opt-in to these logs twice
+ * (i.e. once for firebase, and once in the console), we are sending `DEBUG`
+ * logs to the `console.log` function.
+ */
+const ConsoleMethod = {
+ [LogLevel.DEBUG]: 'log',
+ [LogLevel.VERBOSE]: 'log',
+ [LogLevel.INFO]: 'info',
+ [LogLevel.WARN]: 'warn',
+ [LogLevel.ERROR]: 'error'
+};
+
+/**
+ * The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR
+ * messages on to their corresponding console counterparts (if the log method
+ * is supported by the current log level)
+ */
+const bufferingLogHandler: LogHandler = (instance, logType, ...args): void => {
+ const now = new Date().toISOString();
+
+ // Fail-safe. This is never expected to be true, but if it is,
+ // it's not important enough to throw.
+ if (!logBuffer) {
+ defaultLogHandler(instance, logType, args);
+ return;
+ }
+
+ // Buffer any messages less than the current logLevel
+ if (logType < instance.logLevel) {
+ logBuffer!.add(logType, now, args);
+ return;
+ }
+
+ let codeFound = false;
+ args.forEach(v => {
+ if (typeof v === 'string' && /ID:\s3456/.test(v)) {
+ codeFound = true;
+ }
+ });
+
+ // Buffer any message that do not match the expected code
+ if (!codeFound) {
+ logBuffer!.add(logType, now, args);
+ return;
+ }
+
+ // create identifier that associates all of the associated
+ // context messages with the log message that caused the
+ // flush of the logBuffer
+ const id = generateUniqueDebugId();
+
+ // Optionally write a log message stating if any log messages
+ // were skipped.
+ if (logBuffer.first) {
+ writeLog(instance, id, LogLevel.INFO, logBuffer.first.now, [
+ `... ${logBuffer.numTruncated} log messages skipped ...`
+ ]);
+ }
+
+ // If here, write the log buffer contents as context
+ for (const logInfo of logBuffer) {
+ writeLog(instance, id, logInfo.level, logInfo.now, logInfo.args);
+ }
+ logBuffer.clear();
+
+ // Now write the target log message.
+ writeLog(instance, id, logType, now, args);
+};
+
+function writeLog(
+ instance: Logger,
+ id: string,
+ logType: LogLevel,
+ now: string,
+ args: unknown[]
+): void {
+ const method = ConsoleMethod[logType as keyof typeof ConsoleMethod];
+ if (method) {
+ console[method as 'log' | 'info' | 'warn' | 'error'](
+ `[${now}] (context: ${id}) ${instance.name}:`,
+ ...args
+ );
+ } else {
+ throw new Error(
+ `Attempted to log a message with an invalid logType (value: ${logType})`
+ );
+ }
+}
diff --git a/packages/firestore/test/integration/util/firebase_export.ts b/packages/firestore/test/integration/util/firebase_export.ts
index f58b3ce045b..f8882dadd0e 100644
--- a/packages/firestore/test/integration/util/firebase_export.ts
+++ b/packages/firestore/test/integration/util/firebase_export.ts
@@ -22,13 +22,16 @@
import { FirebaseApp, initializeApp } from '@firebase/app';
-import { Firestore, initializeFirestore } from '../../../src';
+import { Firestore, initializeFirestore, setLogLevel } from '../../../src';
import { PrivateSettings } from '../../../src/lite-api/settings';
// TODO(dimond): Right now we create a new app and Firestore instance for
// every test and never clean them up. We may need to revisit.
let appCount = 0;
+// enable contextual debug logging
+setLogLevel('error', 1000);
+
export function newTestApp(projectId: string, appName?: string): FirebaseApp {
if (appName === undefined) {
appName = 'test-app-' + appCount++;