Skip to content

Commit f7fc7d3

Browse files
authored
fix: Record session duration metric on all session end paths (#78)
* fix: record session duration metric on all session end paths * fix: address review feedback — unused var, add comment on shutdown behavior * fix: prevent double-recording session duration on error+close path * fix: close session explicitly in session error handler to prevent leak
1 parent 403bf51 commit f7fc7d3

2 files changed

Lines changed: 40 additions & 9 deletions

File tree

src/SessionManager.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,25 @@ class SessionManager {
6464
}
6565

6666
/**
67-
* Unregister a session from tracking (called on final cleanup)
67+
* Unregister a session from tracking (called on final cleanup).
68+
* Pass the proxy to record session duration before it is closed.
6869
*/
69-
unregisterSession(sessionId: string | undefined): void {
70+
unregisterSession(sessionId: string | undefined, proxy?: TranscriberProxy): void {
7071
if (!sessionId) return;
7172
const wasActive = this.activeSessions.has(sessionId);
7273
this.activeSessions.delete(sessionId);
7374

74-
// Metrics: decrement active if it was tracked
75+
// Metrics: decrement active if it was tracked, record duration
7576
if (wasActive) {
7677
const instruments = getInstruments();
7778
instruments.sessionsActive.add(-1);
79+
80+
if (proxy) {
81+
const durationSec = proxy.getSessionDurationSec();
82+
if (durationSec > 0) {
83+
instruments.sessionDurationSeconds.record(durationSec);
84+
}
85+
}
7886
}
7987

8088
logger.debug(`Session ${sessionId} unregistered from active sessions (total: ${this.activeSessions.size})`);
@@ -91,10 +99,16 @@ class SessionManager {
9199
return;
92100
}
93101

102+
if (!this.activeSessions.has(sessionId)) {
103+
// Session was already unregistered (e.g. by a prior error handler) — don't detach
104+
logger.debug(`Session ${sessionId} not in active sessions, skipping detach`);
105+
return;
106+
}
107+
94108
if (!config.sessionResumeEnabled) {
95109
// Resume disabled - close immediately
96110
logger.debug(`Session resumption disabled, closing session ${sessionId} immediately`);
97-
this.unregisterSession(sessionId);
111+
this.unregisterSession(sessionId, proxy);
98112
proxy.close();
99113
return;
100114
}
@@ -204,19 +218,35 @@ class SessionManager {
204218
shutdown(): void {
205219
const detachedCount = this.detachedSessions.size;
206220
const activeCount = this.activeSessions.size;
221+
const instruments = getInstruments();
207222

208223
logger.info(`Shutting down SessionManager (detached: ${detachedCount}, active: ${activeCount})`);
209224

210225
// Clean up all detached sessions
211226
for (const [sessionId, session] of this.detachedSessions) {
212227
logger.info(`Cleaning up detached session ${sessionId} during shutdown`);
213228
clearTimeout(session.gracePeriodTimer);
229+
230+
const durationSec = session.transcriberProxy.getSessionDurationSec();
231+
if (durationSec > 0) {
232+
instruments.sessionDurationSeconds.record(durationSec);
233+
}
234+
instruments.sessionsDetached.add(-1);
235+
214236
session.transcriberProxy.close();
215237
}
216238
this.detachedSessions.clear();
217239

218-
// Note: Active sessions will be cleaned up by server.ts through normal close handlers
219-
// Just clear our tracking
240+
// Record duration for active sessions before clearing.
241+
// proxy.close() is intentionally not called here — server.ts close handlers
242+
// will fire when the HTTP server shuts down and handle connection teardown.
243+
for (const proxy of this.activeSessions.values()) {
244+
const durationSec = proxy.getSessionDurationSec();
245+
if (durationSec > 0) {
246+
instruments.sessionDurationSeconds.record(durationSec);
247+
}
248+
instruments.sessionsActive.add(-1);
249+
}
220250
this.activeSessions.clear();
221251

222252
logger.info('SessionManager shutdown complete');

src/server.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ function setupWebSocketEventListeners(ws: WebSocket, session: TranscriberProxy,
9595
sessionManager.detachSession(sessionId, session, connectionId);
9696
} else {
9797
// No sessionId or resumption disabled - close immediately
98-
sessionManager.unregisterSession(sessionId);
98+
sessionManager.unregisterSession(sessionId, session);
9999
session.close();
100100
}
101101
});
@@ -104,7 +104,7 @@ function setupWebSocketEventListeners(ws: WebSocket, session: TranscriberProxy,
104104
ws.addEventListener('error', (event) => {
105105
const errorMessage = 'WebSocket error';
106106
logger.error(`[WS-${connectionId}] Client WebSocket error:`, errorMessage, event);
107-
sessionManager.unregisterSession(sessionId);
107+
sessionManager.unregisterSession(sessionId, session);
108108
session.close();
109109
ws.close(1011, errorMessage);
110110
});
@@ -143,7 +143,8 @@ function setupSessionEventHandlers(ws: WebSocket, session: TranscriberProxy, con
143143
try {
144144
const message = `Error in session ${tag}: ${error instanceof Error ? error.message : String(error)}`;
145145
logger.error(`[WS-${connectionId}] ${message}`);
146-
sessionManager.unregisterSession(sessionId);
146+
sessionManager.unregisterSession(sessionId, session);
147+
session.close();
147148
ws.close(1011, message);
148149
} catch (closeError) {
149150
// Error handlers do not themselves catch errors, so log with logger

0 commit comments

Comments
 (0)