mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
[Part 2/6] feat(telemetry): add activity detector with user interaction tracking (#8111)
This commit is contained in:
164
packages/core/src/telemetry/activity-detector.test.ts
Normal file
164
packages/core/src/telemetry/activity-detector.test.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import {
|
||||
ActivityDetector,
|
||||
getActivityDetector,
|
||||
recordUserActivity,
|
||||
isUserActive,
|
||||
} from './activity-detector.js';
|
||||
|
||||
describe('ActivityDetector', () => {
|
||||
let detector: ActivityDetector;
|
||||
|
||||
beforeEach(() => {
|
||||
detector = new ActivityDetector(1000); // 1 second idle threshold for testing
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with default idle threshold', () => {
|
||||
const defaultDetector = new ActivityDetector();
|
||||
expect(defaultDetector).toBeInstanceOf(ActivityDetector);
|
||||
});
|
||||
|
||||
it('should initialize with custom idle threshold', () => {
|
||||
const customDetector = new ActivityDetector(5000);
|
||||
expect(customDetector).toBeInstanceOf(ActivityDetector);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordActivity', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
it('should update last activity time', () => {
|
||||
const beforeTime = detector.getLastActivityTime();
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
detector.recordActivity();
|
||||
const afterTime = detector.getLastActivityTime();
|
||||
|
||||
expect(afterTime).toBeGreaterThan(beforeTime);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUserActive', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
it('should return true immediately after construction', () => {
|
||||
expect(detector.isUserActive()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true within idle threshold', () => {
|
||||
detector.recordActivity();
|
||||
expect(detector.isUserActive()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false after idle threshold', () => {
|
||||
// Advance time beyond idle threshold
|
||||
vi.advanceTimersByTime(2000); // 2 seconds, threshold is 1 second
|
||||
|
||||
expect(detector.isUserActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true again after recording new activity', () => {
|
||||
// Go idle
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(detector.isUserActive()).toBe(false);
|
||||
|
||||
// Record new activity
|
||||
detector.recordActivity();
|
||||
expect(detector.isUserActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeSinceLastActivity', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
it('should return time elapsed since last activity', () => {
|
||||
detector.recordActivity();
|
||||
vi.advanceTimersByTime(500);
|
||||
|
||||
const timeSince = detector.getTimeSinceLastActivity();
|
||||
expect(timeSince).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLastActivityTime', () => {
|
||||
it('should return the timestamp of last activity', () => {
|
||||
const before = Date.now();
|
||||
detector.recordActivity();
|
||||
const activityTime = detector.getLastActivityTime();
|
||||
const after = Date.now();
|
||||
|
||||
expect(activityTime).toBeGreaterThanOrEqual(before);
|
||||
expect(activityTime).toBeLessThanOrEqual(after);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Global Activity Detector Functions', () => {
|
||||
describe('global instance', () => {
|
||||
it('should expose a global ActivityDetector via getActivityDetector', () => {
|
||||
const detector = getActivityDetector();
|
||||
expect(detector).toBeInstanceOf(ActivityDetector);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActivityDetector', () => {
|
||||
it('should always return the global instance', () => {
|
||||
const detector = getActivityDetector();
|
||||
const detectorAgain = getActivityDetector();
|
||||
expect(detectorAgain).toBe(detector);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordUserActivity', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
it('should record activity on existing detector', () => {
|
||||
const detector = getActivityDetector()!;
|
||||
const beforeTime = detector.getLastActivityTime();
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
recordUserActivity();
|
||||
|
||||
const afterTime = detector.getLastActivityTime();
|
||||
expect(afterTime).toBeGreaterThan(beforeTime);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUserActive', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
it('should reflect global detector state', () => {
|
||||
expect(isUserActive()).toBe(true);
|
||||
// Default idle threshold is 30s; advance beyond it
|
||||
vi.advanceTimersByTime(31000);
|
||||
expect(isUserActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
70
packages/core/src/telemetry/activity-detector.ts
Normal file
70
packages/core/src/telemetry/activity-detector.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tracks user activity state to determine when memory monitoring should be active
|
||||
*/
|
||||
export class ActivityDetector {
|
||||
private lastActivityTime: number = Date.now();
|
||||
private readonly idleThresholdMs: number;
|
||||
|
||||
constructor(idleThresholdMs: number = 30000) {
|
||||
this.idleThresholdMs = idleThresholdMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record user activity (called by CLI when user types, adds messages, etc.)
|
||||
*/
|
||||
recordActivity(): void {
|
||||
this.lastActivityTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is currently active (activity within idle threshold)
|
||||
*/
|
||||
isUserActive(): boolean {
|
||||
const timeSinceActivity = Date.now() - this.lastActivityTime;
|
||||
return timeSinceActivity < this.idleThresholdMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time since last activity in milliseconds
|
||||
*/
|
||||
getTimeSinceLastActivity(): number {
|
||||
return Date.now() - this.lastActivityTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last activity timestamp
|
||||
*/
|
||||
getLastActivityTime(): number {
|
||||
return this.lastActivityTime;
|
||||
}
|
||||
}
|
||||
|
||||
// Global activity detector instance (eagerly created with default threshold)
|
||||
const globalActivityDetector: ActivityDetector = new ActivityDetector();
|
||||
|
||||
/**
|
||||
* Get global activity detector instance
|
||||
*/
|
||||
export function getActivityDetector(): ActivityDetector {
|
||||
return globalActivityDetector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record user activity (convenience function for CLI to call)
|
||||
*/
|
||||
export function recordUserActivity(): void {
|
||||
globalActivityDetector.recordActivity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is currently active (convenience function)
|
||||
*/
|
||||
export function isUserActive(): boolean {
|
||||
return globalActivityDetector.isUserActive();
|
||||
}
|
||||
20
packages/core/src/telemetry/activity-types.ts
Normal file
20
packages/core/src/telemetry/activity-types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Types of user activities that can be tracked
|
||||
*/
|
||||
export enum ActivityType {
|
||||
USER_INPUT_START = 'user_input_start',
|
||||
USER_INPUT_END = 'user_input_end',
|
||||
MESSAGE_ADDED = 'message_added',
|
||||
TOOL_CALL_SCHEDULED = 'tool_call_scheduled',
|
||||
TOOL_CALL_COMPLETED = 'tool_call_completed',
|
||||
STREAM_START = 'stream_start',
|
||||
STREAM_END = 'stream_end',
|
||||
HISTORY_UPDATED = 'history_updated',
|
||||
MANUAL_TRIGGER = 'manual_trigger',
|
||||
}
|
||||
@@ -59,3 +59,10 @@ export { SemanticAttributes } from '@opentelemetry/semantic-conventions';
|
||||
export * from './uiTelemetry.js';
|
||||
export { HighWaterMarkTracker } from './high-water-mark-tracker.js';
|
||||
export { RateLimiter } from './rate-limiter.js';
|
||||
export { ActivityType } from './activity-types.js';
|
||||
export {
|
||||
ActivityDetector,
|
||||
getActivityDetector,
|
||||
recordUserActivity,
|
||||
isUserActive,
|
||||
} from './activity-detector.js';
|
||||
|
||||
Reference in New Issue
Block a user