Skip to content

Commit d137ce6

Browse files
committed
Spy on webchannel for test logging
1 parent badc177 commit d137ce6

File tree

2 files changed

+106
-23
lines changed

2 files changed

+106
-23
lines changed

packages/firestore/src/util/log.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ export { LogLevel, LogLevelString };
2626

2727
const logClient = new Logger('@firebase/firestore');
2828
const defaultLogHandler = logClient.logHandler;
29-
let logBuffer: LogBuffer | undefined;
29+
let logBuffer:
30+
| LogBuffer<{ level: LogLevel; now: string; args: unknown[] }>
31+
| undefined;
3032

3133
// Helper methods are needed because variables can't be exported as read/write
3234
export function getLogLevel(): LogLevel {
@@ -59,7 +61,11 @@ export function setLogLevel(
5961
logClient.setLogLevel(logLevel);
6062

6163
if (includeContext > 0) {
62-
logBuffer = new LogBuffer(includeContext);
64+
logBuffer = new LogBuffer<{
65+
level: LogLevel;
66+
now: string;
67+
args: unknown[];
68+
}>(includeContext);
6369
logClient.logHandler = bufferingLogHandler;
6470
} else {
6571
logBuffer = undefined;
@@ -107,8 +113,8 @@ function argToString(obj: unknown): string | unknown {
107113
}
108114
}
109115

110-
class LogBuffer {
111-
private _buffer: Array<{ level: LogLevel; now: string; args: unknown[] }>;
116+
export class LogBuffer<T> {
117+
private _buffer: T[];
112118
private _numTruncated: number = 0;
113119

114120
constructor(readonly bufferSize: number) {
@@ -132,12 +138,8 @@ class LogBuffer {
132138
* @param now
133139
* @param args
134140
*/
135-
add(level: LogLevel, now: string, args: unknown[]): void {
136-
this._buffer.push({
137-
level,
138-
now,
139-
args
140-
});
141+
add(v: T): void {
142+
this._buffer.push(v);
141143

142144
if (this._buffer.length > this.bufferSize) {
143145
// remove the first (oldest) element
@@ -154,18 +156,14 @@ class LogBuffer {
154156
return this._numTruncated;
155157
}
156158

157-
get first(): { level: LogLevel; now: string; args: unknown[] } | undefined {
159+
get first(): T | undefined {
158160
return this._buffer[0];
159161
}
160162

161163
/**
162164
* Iterate from oldest to newest.
163165
*/
164-
[Symbol.iterator](): Iterator<{
165-
level: LogLevel;
166-
now: string;
167-
args: unknown[];
168-
}> {
166+
[Symbol.iterator](): Iterator<T> {
169167
let currentIndex = 0;
170168
// Create a snapshot of the buffer for iteration.
171169
// This ensures that if the buffer is modified while iterating (e.g., by adding new logs),
@@ -174,11 +172,7 @@ class LogBuffer {
174172
const bufferSnapshot = [...this._buffer];
175173

176174
return {
177-
next: (): IteratorResult<{
178-
level: LogLevel;
179-
now: string;
180-
args: unknown[];
181-
}> => {
175+
next: (): IteratorResult<T> => {
182176
if (currentIndex < bufferSnapshot.length) {
183177
return { value: bufferSnapshot[currentIndex++], done: false };
184178
} else {
@@ -220,7 +214,7 @@ const bufferingLogHandler: LogHandler = (instance, logType, ...args): void => {
220214

221215
// Buffer any messages less than the current logLevel
222216
if (logType < instance.logLevel) {
223-
logBuffer!.add(logType, now, args);
217+
logBuffer!.add({ level: logType, now, args });
224218
return;
225219
}
226220

packages/firestore/test/integration/util/firebase_export.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,102 @@ import { FirebaseApp, initializeApp } from '@firebase/app';
2424

2525
import { Firestore, initializeFirestore, setLogLevel } from '../../../src';
2626
import { PrivateSettings } from '../../../src/lite-api/settings';
27+
import { logDebug } from '../../../src/util/log';
2728

2829
// TODO(dimond): Right now we create a new app and Firestore instance for
2930
// every test and never clean them up. We may need to revisit.
3031
let appCount = 0;
3132

33+
const originalFetch = globalThis.fetch;
34+
35+
/**
36+
* A class that acts as a spy for a ReadableStream.
37+
* It logs the content of the input stream as it's read and then pipes it through.
38+
*/
39+
class ReadableStreamSpy<Uint8Array> {
40+
private inputReadableStream: ReadableStream<Uint8Array>;
41+
private spyTransformStream: TransformStream<Uint8Array, Uint8Array>;
42+
private spiedReadableStream: ReadableStream<Uint8Array>;
43+
44+
private readonly decoder = new TextDecoder();
45+
46+
/**
47+
* Creates an instance of ReadableStreamSpy.
48+
* @param inputReadableStream The ReadableStream to spy on.
49+
*/
50+
constructor(inputReadableStream: ReadableStream<Uint8Array>) {
51+
if (!(inputReadableStream instanceof ReadableStream)) {
52+
throw new Error('Input must be a ReadableStream.');
53+
}
54+
55+
this.inputReadableStream = inputReadableStream;
56+
57+
// Create a TransformStream that logs data
58+
this.spyTransformStream = new TransformStream<Uint8Array, Uint8Array>({
59+
transform: (
60+
chunk: Uint8Array,
61+
controller: TransformStreamDefaultController<Uint8Array>
62+
) => {
63+
// @ts-ignore
64+
logDebug(this.decoder.decode(chunk));
65+
66+
controller.enqueue(chunk); // Pass the chunk along
67+
},
68+
flush: (controller: TransformStreamDefaultController<Uint8Array>) => {
69+
// Any cleanup or final actions if needed
70+
}
71+
});
72+
73+
// Pipe the input stream through the spy transform stream
74+
this.spiedReadableStream = this.inputReadableStream.pipeThrough(
75+
this.spyTransformStream
76+
);
77+
}
78+
79+
/**
80+
* Gets the spied-on ReadableStream.
81+
* You should read from this stream to observe the logged chunks.
82+
* @returns The ReadableStream with spy functionality.
83+
*/
84+
get readableStream(): ReadableStream<Uint8Array> {
85+
return this.spiedReadableStream;
86+
}
87+
}
88+
89+
globalThis.fetch = async function (requestOrUrl, options) {
90+
// @ts-ignore
91+
const url =
92+
typeof requestOrUrl === 'string' ? requestOrUrl : requestOrUrl.url;
93+
94+
logDebug(`FETCH FOR ${url}`);
95+
96+
if (
97+
url.startsWith(
98+
'https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel'
99+
)
100+
) {
101+
const response = await originalFetch(requestOrUrl, options);
102+
103+
if (response.body) {
104+
const spy = new ReadableStreamSpy(response.body);
105+
106+
return Promise.resolve(
107+
new Response(spy.readableStream, {
108+
headers: response.headers,
109+
status: response.status,
110+
statusText: response.statusText
111+
})
112+
);
113+
}
114+
115+
return Promise.resolve(response);
116+
}
117+
118+
return originalFetch(requestOrUrl, options);
119+
};
120+
32121
// enable contextual debug logging
33-
setLogLevel('error', 100);
122+
setLogLevel('error', 200);
34123

35124
export function newTestApp(projectId: string, appName?: string): FirebaseApp {
36125
if (appName === undefined) {

0 commit comments

Comments
 (0)