Skip to content

Commit ff49abb

Browse files
committed
review: quote JSON log fields, reset sniff on reattach, nits
1 parent bfdd2a2 commit ff49abb

2 files changed

Lines changed: 30 additions & 5 deletions

File tree

src/transcriberproxy.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class TranscriberProxy extends EventEmitter {
4545
private audioPacketCount = 0;
4646
private interimTranscriptionCount = 0;
4747
private finalTranscriptionCount = 0;
48-
private firstFrameLoggedTags: Set<string> = new Set();
48+
private firstFrameLoggedTags = new Set<string>();
4949

5050
constructor(ws: WebSocket, options: TranscriberProxyOptions) {
5151
super({ captureRejections: true });
@@ -284,11 +284,15 @@ export class TranscriberProxy extends EventEmitter {
284284
if (hasAudio) {
285285
this.audioPacketCount++;
286286
if (!this.firstFrameLoggedTags.has(tag)) {
287+
// 64 base64 chars decode to at most 48 bytes; we only emit the first 16.
287288
const head = Buffer.from(payloadB64.slice(0, 64), 'base64');
288-
const headHex = head.subarray(0, Math.min(16, head.length)).toString('hex');
289-
const mediaSnapshot = { ...parsedMessage.media, payload: `<b64:${payloadB64.length} chars, first ${headHex.length / 2} decoded bytes=${headHex}>` };
289+
const headByteCount = Math.min(16, head.length);
290+
const headHex = head.subarray(0, headByteCount).toString('hex');
291+
const mediaSnapshot = { ...parsedMessage.media, payload: `<b64:${payloadB64.length} chars, first ${headByteCount} decoded bytes=${headHex}>` };
292+
// JSON-valued fields are quoted so that downstream logfmt-style parsers
293+
// don't misinterpret spaces inside the JSON payload (e.g. inside `tag`).
290294
logger.info(
291-
`First client frame sniff: sessionId=${this.sessionId} tag=${tag} provider=${this.options.provider ?? 'default'} urlEncoding=${this.options.encoding ?? 'opus'} startFormat=${JSON.stringify(connection.getInputFormat())} media=${JSON.stringify(mediaSnapshot)}`,
295+
`First client frame sniff: sessionId=${this.sessionId} tag=${tag} provider=${this.options.provider ?? 'default'} urlEncoding=${this.options.encoding ?? 'opus'} startFormat='${JSON.stringify(connection.getInputFormat())}' media='${JSON.stringify(mediaSnapshot)}'`,
292296
);
293297
this.firstFrameLoggedTags.add(tag);
294298
}
@@ -368,6 +372,11 @@ export class TranscriberProxy extends EventEmitter {
368372
// Re-setup listeners on new WebSocket
369373
this.setupWebSocketListeners();
370374

375+
// Treat a reattach as a new connection for diagnostic purposes: the client
376+
// may negotiate a different audio format on reconnect, so fire the
377+
// first-frame sniff again on the first real audio packet per tag.
378+
this.firstFrameLoggedTags.clear();
379+
371380
// Reset chunk tracking on all connections so frames from the new client
372381
// aren't discarded as "reordered" (chunk numbers restart from 0)
373382
this.outgoingConnections.forEach((connection, tag) => {

test/unit/TranscriberProxy.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ vi.mock('../../src/OutgoingConnection', () => ({
5353
this.addTranscriptContext = vi.fn();
5454
this.updateInputFormat = vi.fn();
5555
this.getInputFormat = vi.fn(() => inputFormat ?? { encoding: 'opus' });
56+
this.resetChunkTracking = vi.fn();
5657
this.close = vi.fn();
5758
this.onInterimTranscription = undefined;
5859
this.onCompleteTranscription = undefined;
@@ -599,7 +600,7 @@ describe('TranscriberProxy', () => {
599600
const msg = sniffCalls[0][0] as string;
600601
expect(msg).toContain('tag=tag1');
601602
expect(msg).toContain('urlEncoding=opus');
602-
expect(msg).toContain('startFormat={"encoding":"opus"}');
603+
expect(msg).toContain(`startFormat='{"encoding":"opus"}'`);
603604
expect(msg).toContain('4f676753'); // 'OggS' in hex
604605
expect(msg).toContain(`<b64:${OGG_PAYLOAD.length} chars, first 4 decoded bytes=4f676753>`);
605606
});
@@ -647,6 +648,21 @@ describe('TranscriberProxy', () => {
647648
expect(endCall?.[0]).toContain('audioPackets=1');
648649
});
649650

651+
it('fires the first-frame sniff again after a WebSocket reattach', () => {
652+
const proxy = new TranscriberProxy(mockWebSocket, options);
653+
proxy.handleStartEvent({ event: 'start', start: { tag: 'tag1', mediaFormat: { encoding: 'opus' } } });
654+
655+
proxy.handleMediaEvent({ event: 'media', media: { tag: 'tag1', payload: OGG_PAYLOAD, chunk: 0, timestamp: 0 } });
656+
proxy.handleMediaEvent({ event: 'media', media: { tag: 'tag1', payload: OGG_PAYLOAD, chunk: 1, timestamp: 0 } });
657+
658+
vi.mocked(logger.info).mockClear();
659+
proxy.reattachWebSocket({ addEventListener: vi.fn(), send: vi.fn(), close: vi.fn() } as any);
660+
661+
proxy.handleMediaEvent({ event: 'media', media: { tag: 'tag1', payload: OGG_PAYLOAD, chunk: 0, timestamp: 0 } });
662+
const sniffCalls = vi.mocked(logger.info).mock.calls.filter(([msg]) => typeof msg === 'string' && msg.startsWith('First client frame sniff:'));
663+
expect(sniffCalls).toHaveLength(1);
664+
});
665+
650666
it('emits a session-end summary with audioPackets, interims, finals, and provider', () => {
651667
const proxy = new TranscriberProxy(mockWebSocket, { ...options, provider: 'deepgram' });
652668
proxy.handleStartEvent({ event: 'start', start: { tag: 'tag1', mediaFormat: { encoding: 'opus' } } });

0 commit comments

Comments
 (0)