🔧 Miscellaneous Improvements and Refactoring (#466)

This commit is contained in:
tanzhenxin
2025-08-27 17:32:57 +08:00
committed by GitHub
parent 347e606366
commit 600c58bbcb
16 changed files with 755 additions and 81 deletions

View File

@@ -8,12 +8,23 @@
* Integration test to verify circular reference handling with proxy agents
*/
import { describe, it, expect } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { QwenLogger } from './qwen-logger/qwen-logger.js';
import { RumEvent } from './qwen-logger/event-types.js';
import { Config } from '../config/config.js';
describe('Circular Reference Integration Test', () => {
beforeEach(() => {
// Clear singleton instance before each test
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(QwenLogger as any).instance = undefined;
});
afterEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(QwenLogger as any).instance = undefined;
});
it('should handle HttpsProxyAgent-like circular references in qwen logging', () => {
// Create a mock config with proxy
const mockConfig = {
@@ -64,4 +75,36 @@ describe('Circular Reference Integration Test', () => {
logger?.enqueueLogEvent(problematicEvent);
}).not.toThrow();
});
it('should handle event overflow without memory leaks', () => {
const mockConfig = {
getTelemetryEnabled: () => true,
getUsageStatisticsEnabled: () => true,
getSessionId: () => 'test-session',
getDebugMode: () => true,
} as unknown as Config;
const logger = QwenLogger.getInstance(mockConfig);
// Add more events than the maximum capacity
for (let i = 0; i < 1100; i++) {
logger?.enqueueLogEvent({
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: `overflow-test-${i}`,
});
}
// Logger should still be functional
expect(logger).toBeDefined();
expect(() => {
logger?.enqueueLogEvent({
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: 'final-test',
});
}).not.toThrow();
});
});

View File

@@ -8,7 +8,6 @@ import { LogAttributes, LogRecord, logs } from '@opentelemetry/api-logs';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { Config } from '../config/config.js';
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
import {
EVENT_API_ERROR,
EVENT_API_REQUEST,
@@ -150,7 +149,7 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
}
export function logApiRequest(config: Config, event: ApiRequestEvent): void {
QwenLogger.getInstance(config)?.logApiRequestEvent(event);
// QwenLogger.getInstance(config)?.logApiRequestEvent(event);
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
@@ -364,6 +363,7 @@ export function logIdeConnection(
config: Config,
event: IdeConnectionEvent,
): void {
QwenLogger.getInstance(config)?.logIdeConnectionEvent(event);
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
@@ -384,7 +384,7 @@ export function logKittySequenceOverflow(
config: Config,
event: KittySequenceOverflowEvent,
): void {
ClearcutLogger.getInstance(config)?.logKittySequenceOverflowEvent(event);
QwenLogger.getInstance(config)?.logKittySequenceOverflowEvent(event);
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...getCommonAttributes(config),

View File

@@ -79,5 +79,6 @@ export interface RumPayload {
session: RumSession;
view: RumView;
events: RumEvent[];
properties?: Record<string, unknown>;
_v: string;
}

View File

@@ -0,0 +1,407 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
afterAll,
} from 'vitest';
import { QwenLogger, TEST_ONLY } from './qwen-logger.js';
import { Config } from '../../config/config.js';
import {
StartSessionEvent,
EndSessionEvent,
IdeConnectionEvent,
KittySequenceOverflowEvent,
IdeConnectionType,
} from '../types.js';
import { RumEvent } from './event-types.js';
// Mock dependencies
vi.mock('../../utils/user_id.js', () => ({
getInstallationId: vi.fn(() => 'test-installation-id'),
}));
vi.mock('../../utils/safeJsonStringify.js', () => ({
safeJsonStringify: vi.fn((obj) => JSON.stringify(obj)),
}));
// Mock https module
vi.mock('https', () => ({
request: vi.fn(),
}));
const makeFakeConfig = (overrides: Partial<Config> = {}): Config => {
const defaults = {
getUsageStatisticsEnabled: () => true,
getDebugMode: () => false,
getSessionId: () => 'test-session-id',
getCliVersion: () => '1.0.0',
getProxy: () => undefined,
getContentGeneratorConfig: () => ({ authType: 'test-auth' }),
getMcpServers: () => ({}),
getModel: () => 'test-model',
getEmbeddingModel: () => 'test-embedding',
getSandbox: () => false,
getCoreTools: () => [],
getApprovalMode: () => 'auto',
getTelemetryEnabled: () => true,
getTelemetryLogPromptsEnabled: () => false,
getFileFilteringRespectGitIgnore: () => true,
...overrides,
};
return defaults as Config;
};
describe('QwenLogger', () => {
let mockConfig: Config;
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-01-01T12:00:00.000Z'));
mockConfig = makeFakeConfig();
// Clear singleton instance
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(QwenLogger as any).instance = undefined;
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
afterAll(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(QwenLogger as any).instance = undefined;
});
describe('getInstance', () => {
it('returns undefined when usage statistics are disabled', () => {
const config = makeFakeConfig({ getUsageStatisticsEnabled: () => false });
const logger = QwenLogger.getInstance(config);
expect(logger).toBeUndefined();
});
it('returns an instance when usage statistics are enabled', () => {
const logger = QwenLogger.getInstance(mockConfig);
expect(logger).toBeInstanceOf(QwenLogger);
});
it('is a singleton', () => {
const logger1 = QwenLogger.getInstance(mockConfig);
const logger2 = QwenLogger.getInstance(mockConfig);
expect(logger1).toBe(logger2);
});
});
describe('event queue management', () => {
it('should handle event overflow gracefully', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
// Fill the queue beyond capacity
for (let i = 0; i < TEST_ONLY.MAX_EVENTS + 10; i++) {
logger.enqueueLogEvent({
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: `test-event-${i}`,
});
}
// Should have logged debug messages about dropping events
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'QwenLogger: Dropped old event to prevent memory leak',
),
);
});
it('should handle enqueue errors gracefully', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
// Mock the events deque to throw an error
const originalPush = logger['events'].push;
logger['events'].push = vi.fn(() => {
throw new Error('Test error');
});
logger.enqueueLogEvent({
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: 'test-event',
});
expect(consoleSpy).toHaveBeenCalledWith(
'QwenLogger: Failed to enqueue log event.',
expect.any(Error),
);
// Restore original method
logger['events'].push = originalPush;
});
});
describe('concurrent flush protection', () => {
it('should handle concurrent flush requests', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
// Manually set the flush in progress flag to simulate concurrent access
logger['isFlushInProgress'] = true;
// Try to flush while another flush is in progress
const result = logger.flushToRum();
// Should have logged about pending flush
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'QwenLogger: Flush already in progress, marking pending flush',
),
);
// Should return a resolved promise
expect(result).toBeInstanceOf(Promise);
// Reset the flag
logger['isFlushInProgress'] = false;
});
});
describe('failed event retry mechanism', () => {
it('should requeue failed events with size limits', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
const failedEvents: RumEvent[] = [];
for (let i = 0; i < TEST_ONLY.MAX_RETRY_EVENTS + 50; i++) {
failedEvents.push({
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: `failed-event-${i}`,
});
}
// Call the private method using bracket notation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(logger as any).requeueFailedEvents(failedEvents);
// Should have logged about dropping events due to retry limit
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('QwenLogger: Re-queued'),
);
});
it('should handle empty retry queue gracefully', () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
// Fill the queue to capacity first
for (let i = 0; i < TEST_ONLY.MAX_EVENTS; i++) {
logger.enqueueLogEvent({
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: `event-${i}`,
});
}
// Try to requeue when no space is available
const failedEvents: RumEvent[] = [
{
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: 'failed-event',
},
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(logger as any).requeueFailedEvents(failedEvents);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('QwenLogger: No events re-queued'),
);
});
});
describe('event handlers', () => {
it('should log IDE connection events', () => {
const logger = QwenLogger.getInstance(mockConfig)!;
const enqueueSpy = vi.spyOn(logger, 'enqueueLogEvent');
const event = new IdeConnectionEvent(IdeConnectionType.SESSION);
logger.logIdeConnectionEvent(event);
expect(enqueueSpy).toHaveBeenCalledWith(
expect.objectContaining({
event_type: 'action',
type: 'connection',
name: 'ide_connection',
snapshots: JSON.stringify({
connection_type: IdeConnectionType.SESSION,
}),
}),
);
});
it('should log Kitty sequence overflow events', () => {
const logger = QwenLogger.getInstance(mockConfig)!;
const enqueueSpy = vi.spyOn(logger, 'enqueueLogEvent');
const event = new KittySequenceOverflowEvent(1024, 'truncated...');
logger.logKittySequenceOverflowEvent(event);
expect(enqueueSpy).toHaveBeenCalledWith(
expect.objectContaining({
event_type: 'exception',
type: 'overflow',
name: 'kitty_sequence_overflow',
subtype: 'kitty_sequence_overflow',
snapshots: JSON.stringify({
sequence_length: 1024,
truncated_sequence: 'truncated...',
}),
}),
);
});
it('should flush start session events immediately', async () => {
const logger = QwenLogger.getInstance(mockConfig)!;
const flushSpy = vi.spyOn(logger, 'flushToRum').mockResolvedValue({});
const testConfig = makeFakeConfig({
getModel: () => 'test-model',
getEmbeddingModel: () => 'test-embedding',
});
const event = new StartSessionEvent(testConfig);
logger.logStartSessionEvent(event);
expect(flushSpy).toHaveBeenCalled();
});
it('should flush end session events immediately', async () => {
const logger = QwenLogger.getInstance(mockConfig)!;
const flushSpy = vi.spyOn(logger, 'flushToRum').mockResolvedValue({});
const event = new EndSessionEvent(mockConfig);
logger.logEndSessionEvent(event);
expect(flushSpy).toHaveBeenCalled();
});
});
describe('flush timing', () => {
it('should not flush if interval has not passed', () => {
const logger = QwenLogger.getInstance(mockConfig)!;
const flushSpy = vi.spyOn(logger, 'flushToRum');
// Add an event and try to flush immediately
logger.enqueueLogEvent({
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: 'test-event',
});
logger.flushIfNeeded();
expect(flushSpy).not.toHaveBeenCalled();
});
it('should flush when interval has passed', () => {
const logger = QwenLogger.getInstance(mockConfig)!;
const flushSpy = vi.spyOn(logger, 'flushToRum').mockResolvedValue({});
// Add an event
logger.enqueueLogEvent({
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: 'test-event',
});
// Advance time beyond flush interval
vi.advanceTimersByTime(TEST_ONLY.FLUSH_INTERVAL_MS + 1000);
logger.flushIfNeeded();
expect(flushSpy).toHaveBeenCalled();
});
});
describe('error handling', () => {
it('should handle flush errors gracefully with debug mode', async () => {
const debugConfig = makeFakeConfig({ getDebugMode: () => true });
const logger = QwenLogger.getInstance(debugConfig)!;
const consoleSpy = vi
.spyOn(console, 'debug')
.mockImplementation(() => {});
// Add an event first
logger.enqueueLogEvent({
timestamp: Date.now(),
event_type: 'action',
type: 'test',
name: 'test-event',
});
// Mock flushToRum to throw an error
const originalFlush = logger.flushToRum.bind(logger);
logger.flushToRum = vi.fn().mockRejectedValue(new Error('Network error'));
// Advance time to trigger flush
vi.advanceTimersByTime(TEST_ONLY.FLUSH_INTERVAL_MS + 1000);
logger.flushIfNeeded();
// Wait for async operations
await vi.runAllTimersAsync();
expect(consoleSpy).toHaveBeenCalledWith(
'Error flushing to RUM:',
expect.any(Error),
);
// Restore original method
logger.flushToRum = originalFlush;
});
});
describe('constants export', () => {
it('should export test constants', () => {
expect(TEST_ONLY.MAX_EVENTS).toBe(1000);
expect(TEST_ONLY.MAX_RETRY_EVENTS).toBe(100);
expect(TEST_ONLY.FLUSH_INTERVAL_MS).toBe(60000);
});
});
});

View File

@@ -7,7 +7,6 @@
import { Buffer } from 'buffer';
import * as https from 'https';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { randomUUID } from 'crypto';
import {
StartSessionEvent,
@@ -22,6 +21,8 @@ import {
NextSpeakerCheckEvent,
SlashCommandEvent,
MalformedJsonResponseEvent,
IdeConnectionEvent,
KittySequenceOverflowEvent,
} from '../types.js';
import {
RumEvent,
@@ -31,12 +32,12 @@ import {
RumExceptionEvent,
RumPayload,
} from './event-types.js';
// Removed unused EventMetadataKey import
import { Config } from '../../config/config.js';
import { safeJsonStringify } from '../../utils/safeJsonStringify.js';
// Removed unused import
import { HttpError, retryWithBackoff } from '../../utils/retry.js';
import { getInstallationId } from '../../utils/user_id.js';
import { FixedDeque } from 'mnemonist';
import { AuthType } from '../../core/contentGenerator.js';
// Usage statistics collection endpoint
const USAGE_STATS_HOSTNAME = 'gb4w8c3ygj-default-sea.rum.aliyuncs.com';
@@ -44,6 +45,23 @@ const USAGE_STATS_PATH = '/';
const RUN_APP_ID = 'gb4w8c3ygj@851d5d500f08f92';
/**
* Interval in which buffered events are sent to RUM.
*/
const FLUSH_INTERVAL_MS = 1000 * 60;
/**
* Maximum amount of events to keep in memory. Events added after this amount
* are dropped until the next flush to RUM, which happens periodically as
* defined by {@link FLUSH_INTERVAL_MS}.
*/
const MAX_EVENTS = 1000;
/**
* Maximum events to retry after a failed RUM flush
*/
const MAX_RETRY_EVENTS = 100;
export interface LogResponse {
nextRequestWaitMs?: number;
}
@@ -53,23 +71,42 @@ export interface LogResponse {
export class QwenLogger {
private static instance: QwenLogger;
private config?: Config;
private readonly events: RumEvent[] = [];
private last_flush_time: number = Date.now();
private flush_interval_ms: number = 1000 * 60; // Wait at least a minute before flushing events.
/**
* Queue of pending events that need to be flushed to the server. New events
* are added to this queue and then flushed on demand (via `flushToRum`)
*/
private readonly events: FixedDeque<RumEvent>;
/**
* The last time that the events were successfully flushed to the server.
*/
private lastFlushTime: number = Date.now();
private userId: string;
private sessionId: string;
private viewId: string;
/**
* The value is true when there is a pending flush happening. This prevents
* concurrent flush operations.
*/
private isFlushInProgress: boolean = false;
/**
* This value is true when a flush was requested during an ongoing flush.
*/
private pendingFlush: boolean = false;
private isShutdown: boolean = false;
private constructor(config?: Config) {
this.config = config;
this.events = new FixedDeque<RumEvent>(Array, MAX_EVENTS);
this.userId = this.generateUserId();
this.sessionId =
typeof this.config?.getSessionId === 'function'
? this.config.getSessionId()
: '';
this.viewId = randomUUID();
}
private generateUserId(): string {
@@ -92,7 +129,26 @@ export class QwenLogger {
}
enqueueLogEvent(event: RumEvent): void {
this.events.push(event);
try {
// Manually handle overflow for FixedDeque, which throws when full.
const wasAtCapacity = this.events.size >= MAX_EVENTS;
if (wasAtCapacity) {
this.events.shift(); // Evict oldest element to make space.
}
this.events.push(event);
if (wasAtCapacity && this.config?.getDebugMode()) {
console.debug(
`QwenLogger: Dropped old event to prevent memory leak (queue size: ${this.events.size})`,
);
}
} catch (error) {
if (this.config?.getDebugMode()) {
console.error('QwenLogger: Failed to enqueue log event.', error);
}
}
}
createRumEvent(
@@ -143,6 +199,7 @@ export class QwenLogger {
}
async createRumPayload(): Promise<RumPayload> {
const authType = this.config?.getAuthType();
const version = this.config?.getCliVersion() || 'unknown';
return {
@@ -159,40 +216,59 @@ export class QwenLogger {
id: this.sessionId,
},
view: {
id: this.viewId,
id: this.sessionId,
name: 'qwen-code-cli',
},
events: [...this.events],
events: this.events.toArray() as RumEvent[],
properties: {
auth_type: authType,
model: this.config?.getModel(),
base_url:
authType === AuthType.USE_OPENAI ? process.env.OPENAI_BASE_URL : '',
},
_v: `qwen-code@${version}`,
};
}
flushIfNeeded(): void {
if (Date.now() - this.last_flush_time < this.flush_interval_ms) {
return;
}
// Prevent concurrent flush operations
if (this.isFlushInProgress) {
if (Date.now() - this.lastFlushTime < FLUSH_INTERVAL_MS) {
return;
}
this.flushToRum().catch((error) => {
console.debug('Error flushing to RUM:', error);
if (this.config?.getDebugMode()) {
console.debug('Error flushing to RUM:', error);
}
});
}
async flushToRum(): Promise<LogResponse> {
if (this.isFlushInProgress) {
if (this.config?.getDebugMode()) {
console.debug(
'QwenLogger: Flush already in progress, marking pending flush.',
);
}
this.pendingFlush = true;
return Promise.resolve({});
}
this.isFlushInProgress = true;
if (this.config?.getDebugMode()) {
console.log('Flushing log events to RUM.');
}
if (this.events.length === 0) {
if (this.events.size === 0) {
this.isFlushInProgress = false;
return {};
}
this.isFlushInProgress = true;
const eventsToSend = this.events.toArray() as RumEvent[];
this.events.clear();
const rumPayload = await this.createRumPayload();
// Override events with the ones we're sending
rumPayload.events = eventsToSend;
const flushFn = () =>
new Promise<Buffer>((resolve, reject) => {
const body = safeJsonStringify(rumPayload);
@@ -246,16 +322,29 @@ export class QwenLogger {
},
});
this.events.splice(0, this.events.length);
this.last_flush_time = Date.now();
this.lastFlushTime = Date.now();
return {};
} catch (error) {
if (this.config?.getDebugMode()) {
console.error('RUM flush failed after multiple retries.', error);
}
// Re-queue failed events for retry
this.requeueFailedEvents(eventsToSend);
return {};
} finally {
this.isFlushInProgress = false;
// If a flush was requested while we were flushing, flush again
if (this.pendingFlush) {
this.pendingFlush = false;
// Fire and forget the pending flush
this.flushToRum().catch((error) => {
if (this.config?.getDebugMode()) {
console.debug('Error in pending flush to RUM:', error);
}
});
}
}
}
@@ -282,7 +371,9 @@ export class QwenLogger {
// Flush start event immediately
this.enqueueLogEvent(applicationEvent);
this.flushToRum().catch((error: unknown) => {
console.debug('Error flushing to RUM:', error);
if (this.config?.getDebugMode()) {
console.debug('Error flushing to RUM:', error);
}
});
}
@@ -451,13 +542,41 @@ export class QwenLogger {
this.flushIfNeeded();
}
logIdeConnectionEvent(event: IdeConnectionEvent): void {
const rumEvent = this.createActionEvent('connection', 'ide_connection', {
snapshots: JSON.stringify({ connection_type: event.connection_type }),
});
this.enqueueLogEvent(rumEvent);
this.flushIfNeeded();
}
logKittySequenceOverflowEvent(event: KittySequenceOverflowEvent): void {
const rumEvent = this.createExceptionEvent(
'overflow',
'kitty_sequence_overflow',
{
subtype: 'kitty_sequence_overflow',
snapshots: JSON.stringify({
sequence_length: event.sequence_length,
truncated_sequence: event.truncated_sequence,
}),
},
);
this.enqueueLogEvent(rumEvent);
this.flushIfNeeded();
}
logEndSessionEvent(_event: EndSessionEvent): void {
const applicationEvent = this.createViewEvent('session', 'session_end', {});
// Flush immediately on session end.
this.enqueueLogEvent(applicationEvent);
this.flushToRum().catch((error: unknown) => {
console.debug('Error flushing to RUM:', error);
if (this.config?.getDebugMode()) {
console.debug('Error flushing to RUM:', error);
}
});
}
@@ -480,4 +599,60 @@ export class QwenLogger {
const event = new EndSessionEvent(this.config);
this.logEndSessionEvent(event);
}
private requeueFailedEvents(eventsToSend: RumEvent[]): void {
// Add the events back to the front of the queue to be retried, but limit retry queue size
const eventsToRetry = eventsToSend.slice(-MAX_RETRY_EVENTS); // Keep only the most recent events
// Log a warning if we're dropping events
if (eventsToSend.length > MAX_RETRY_EVENTS && this.config?.getDebugMode()) {
console.warn(
`QwenLogger: Dropping ${
eventsToSend.length - MAX_RETRY_EVENTS
} events due to retry queue limit. Total events: ${
eventsToSend.length
}, keeping: ${MAX_RETRY_EVENTS}`,
);
}
// Determine how many events can be re-queued
const availableSpace = MAX_EVENTS - this.events.size;
const numEventsToRequeue = Math.min(eventsToRetry.length, availableSpace);
if (numEventsToRequeue === 0) {
if (this.config?.getDebugMode()) {
console.debug(
`QwenLogger: No events re-queued (queue size: ${this.events.size})`,
);
}
return;
}
// Get the most recent events to re-queue
const eventsToRequeue = eventsToRetry.slice(
eventsToRetry.length - numEventsToRequeue,
);
// Prepend events to the front of the deque to be retried first.
// We iterate backwards to maintain the original order of the failed events.
for (let i = eventsToRequeue.length - 1; i >= 0; i--) {
this.events.unshift(eventsToRequeue[i]);
}
// Clear any potential overflow
while (this.events.size > MAX_EVENTS) {
this.events.pop();
}
if (this.config?.getDebugMode()) {
console.debug(
`QwenLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`,
);
}
}
}
export const TEST_ONLY = {
MAX_RETRY_EVENTS,
MAX_EVENTS,
FLUSH_INTERVAL_MS,
};