Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ea8f007
feat(meetings,llm): add webinar LLM connect and locus sync latency di…
Tianhui-Han May 26, 2026
47d0606
fix(meetings): emit breakout join response metric once per breakoutMo…
Tianhui-Han May 26, 2026
838cc6f
Merge branch 'next' into feat/webinar_LLM_Latencies_2
Tianhui-Han May 26, 2026
1b82b71
feat(meetings): add Locus sync latency metric callbacks
Tianhui-Han May 28, 2026
f15504d
refactor(meetings): simplify LLM metric payload construction
Tianhui-Han May 28, 2026
3335dc8
refactor(metrics): align locus sync latency tracking with internal ti…
Tianhui-Han May 28, 2026
35ba521
fix(metrics): record sync HTTP latency timestamps at request boundaries
Tianhui-Han May 28, 2026
e8311a9
refactor(metrics): simplify sync backoff tracking
Tianhui-Han May 28, 2026
09d121a
Merge branch 'next' into feat/webinar_LLM_Latencies_2
Tianhui-Han Jun 1, 2026
f9875b0
fix(meetings): preserve breakout join metric without LLM latency
Tianhui-Han Jun 1, 2026
a4dbd7d
fix(meetings): preserve watchdog backoff in sync metrics
Tianhui-Han Jun 1, 2026
dc5d4e7
fix(meetings): retain sync metrics for empty sync responses
Tianhui-Han Jun 1, 2026
b06fcd0
fix(meetings): preserve syncAll backoff in sync metrics
Tianhui-Han Jun 1, 2026
d151ef7
fix(metrics): scope locus sync latency by meeting
Tianhui-Han Jun 1, 2026
b243dec
fix(meetings): dedupe breakout join response metrics
Tianhui-Han Jun 1, 2026
7ceadf9
fix(meetings): track target sync version for metrics
Tianhui-Han Jun 1, 2026
21e57ee
fix(meetings): avoid overwriting pending sync metrics
Tianhui-Han Jun 1, 2026
839c876
Merge branch 'next' into feat/webinar_LLM_Latencies_2
Tianhui-Han Jun 1, 2026
f34a260
fix(meetings): preserve root sync backoff metrics
Tianhui-Han Jun 1, 2026
a36871d
fix(meetings): keep breakout LLM join metric from being deduped
Tianhui-Han Jun 1, 2026
0d236ca
Merge branch 'next' into feat/webinar_LLM_Latencies_2
Tianhui-Han Jun 2, 2026
70c8b96
fix: use heartbeat target version for single-leaf sync metrics
Tianhui-Han Jun 2, 2026
0d54758
fix: add ts-ignore for webex property injected by registerPlugin
Tianhui-Han Jun 2, 2026
97b7bff
fix: correlate sync metrics by LLM tracking id
Tianhui-Han Jun 3, 2026
c3b1a10
test(llm): assert registerAndConnect latency values precisely
Tianhui-Han Jun 3, 2026
608910a
fix(metrics): require meeting id for locus sync latencies
Tianhui-Han Jun 3, 2026
36a6303
refactor(metrics): share locus sync latency event definitions
Tianhui-Han Jun 3, 2026
528c821
refactor(meetings): use callbacks sync latency tracker directly
Tianhui-Han Jun 3, 2026
a0ef7ee
refactor(meetings): use updateMeeting callback from callbacks
Tianhui-Han Jun 3, 2026
4356f3d
fix(meetings): guard breakout LLM join metrics
Tianhui-Han Jun 4, 2026
444d4c5
refactor(meetings): simplify LLM metric payload construction
Tianhui-Han Jun 4, 2026
051bbd7
fix(plugin-meetings): report locus sync complete metrics from parser …
Tianhui-Han Jun 5, 2026
fdddb7f
fix(metrics): complete locus sync latency via sync response callback
Tianhui-Han Jun 8, 2026
051343e
Merge branch 'next' into feat/webinar_LLM_Latencies_2
Tianhui-Han Jun 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/@webex/internal-plugin-llm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as WebexCore from '@webex/webex-core';
import LLMChannel, {config} from './llm';
import {DataChannelTokenType} from './llm.types';

export type {RegisterAndConnectTiming} from './llm.types';

WebexCore.registerInternalPlugin('llm', LLMChannel, {
config,
});
Expand Down
31 changes: 28 additions & 3 deletions packages/@webex/internal-plugin-llm/src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
SUBSCRIPTION_AWARE_SUBCHANNELS_PARAM,
LLM_DEFAULT_SESSION,
} from './constants';
import {ILLMChannel, DataChannelTokenType} from './llm.types';
import {ILLMChannel, DataChannelTokenType, RegisterAndConnectTiming} from './llm.types';

export const config = {
llm: {
Expand Down Expand Up @@ -125,7 +125,9 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
datachannelUrl: string,
datachannelToken?: string,
sessionId: string = LLM_DEFAULT_SESSION
): Promise<void> => {
): Promise<RegisterAndConnectTiming | undefined> => {
const registerStart = performance.now();

// Pre-populate locusUrl and datachannelUrl before register() fires the
// HTTP POST, so that any token refresh triggered during registration can
// be routed via connections without falling back to a locusInfo URL scan.
Expand All @@ -139,18 +141,30 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
return this.register(datachannelUrl, datachannelToken, sessionId).then(async () => {
if (!locusUrl || !datachannelUrl) return undefined;

const clientLLMDatachannelResponseTime = Math.round(performance.now() - registerStart);

// locusUrl and datachannelUrl were pre-populated before register(); here
// we only need to read the existing session data to get webSocketUrl/binding
// that register() filled in.
const sessionData = this.connections.get(sessionId) || {};

if (!sessionData.webSocketUrl) {
return undefined;
}

const isDataChannelTokenEnabled = await this.isDataChannelTokenEnabled();

const connectUrl = isDataChannelTokenEnabled
? LLMChannel.buildUrlWithAwareSubchannels(sessionData.webSocketUrl, AWARE_DATA_CHANNEL)
: sessionData.webSocketUrl;

return this.connect(connectUrl, sessionId);
const connectStart = performance.now();

await this.connect(connectUrl, sessionId);

Comment on lines +161 to +164

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve datachannel latency when websocket connect fails

If the datachannel registration succeeds but the websocket connect() rejects, this method rejects before returning the already-measured clientLLMDatachannelResponseTime; the caller’s catch path then reports both LLM connect latencies as 0. In websocket-failure scenarios this corrupts the new client.llm.connect.response failure telemetry by losing the registration response time that was actually observed.

Useful? React with 👍 / 👎.

const clientLLMWebSocketConnectTime = Math.round(performance.now() - connectStart);

return {clientLLMDatachannelResponseTime, clientLLMWebSocketConnectTime};
});
};

Expand Down Expand Up @@ -198,6 +212,17 @@ export default class LLMChannel extends (Mercury as any) implements ILLMChannel
return sessionData?.datachannelUrl;
};

/**
* Get WebSocket URL for the connection
* @param {string} sessionId - Connection identifier
* @returns {string | undefined} WebSocket URL
*/
public getWebSocketUrl = (sessionId = LLM_DEFAULT_SESSION): string | undefined => {
const sessionData = this.connections.get(sessionId);

return sessionData?.webSocketUrl;
};

/**
* Set the owner meeting ID for a given LLM session. Used by the meetings
* plugin to tag which Meeting instance currently owns the (default) LLM
Expand Down
13 changes: 11 additions & 2 deletions packages/@webex/internal-plugin-llm/src/llm.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@ export enum DataChannelTokenType {

type DataChannelTokenKey = DataChannelTokenType | string;

/**
* Latencies (in milliseconds) captured during register + websocket connect.
*/
type RegisterAndConnectTiming = {
clientLLMDatachannelResponseTime?: number;
clientLLMWebSocketConnectTime?: number;
};

interface ILLMChannel {
registerAndConnect: (
locusUrl: string,
datachannelUrl: string,
datachannelToken?: string,
sessionId?: string
) => Promise<void>;
) => Promise<RegisterAndConnectTiming | undefined>;
isConnected: (sessionId?: string) => boolean;
getBinding: (sessionId?: string) => string;
getLocusUrl: (sessionId?: string) => string;
getDatachannelUrl: (sessionId?: string) => string;
getWebSocketUrl: (sessionId?: string) => string | undefined;
disconnectLLM: (
options: {code: number; reason: string},
sessionId?: string,
Expand Down Expand Up @@ -66,4 +75,4 @@ interface ILLMChannel {
}

// eslint-disable-next-line import/prefer-default-export
export type {ILLMChannel, DataChannelTokenKey};
export type {ILLMChannel, DataChannelTokenKey, RegisterAndConnectTiming};
52 changes: 52 additions & 0 deletions packages/@webex/internal-plugin-llm/test/unit/spec/llm.js
Original file line number Diff line number Diff line change
Expand Up @@ -709,5 +709,57 @@ describe('plugin-llm', () => {
});
});

describe('#registerAndConnect timing', () => {
it('returns timing data on successful connection', async () => {
llmService.register = sinon.stub().callsFake(async () => {
const sessionData = llmService.connections.get('llm-default-session') || {};

sessionData.webSocketUrl = 'wss://example.com/socket';
sessionData.binding = 'binding';
llmService.connections.set('llm-default-session', sessionData);
});

const result = await llmService.registerAndConnect(locusUrl, datachannelUrl, undefined);

assert.isDefined(result);
assert.isNumber(result.clientLLMDatachannelResponseTime);
assert.isNumber(result.clientLLMWebSocketConnectTime);
assert.isAtLeast(result.clientLLMDatachannelResponseTime, 0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check is not good enough, we can control time progression with sinon.useFakeTimers() so we can have a precise check that the returned time values are correct and also not mixed up

@Tianhui-Han Tianhui-Han Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

assert.isAtLeast(result.clientLLMWebSocketConnectTime, 0);
});

it('returns undefined when locusUrl is empty', async () => {
llmService.register = sinon.stub().resolves();

const result = await llmService.registerAndConnect('', datachannelUrl, undefined);

assert.isUndefined(result);
});
});

describe('#getWebSocketUrl', () => {
it('returns the websocket URL for default session', () => {
llmService.connections.set('llm-default-session', {
webSocketUrl: 'wss://test.example.com/ws',
});

assert.equal(llmService.getWebSocketUrl(), 'wss://test.example.com/ws');
});

it('returns undefined when no connection exists', () => {
llmService.connections.clear();

assert.isUndefined(llmService.getWebSocketUrl());
});

it('returns the websocket URL for custom session', () => {
llmService.connections.set('custom-session', {
webSocketUrl: 'wss://custom.example.com/ws',
});

assert.equal(llmService.getWebSocketUrl('custom-session'), 'wss://custom.example.com/ws');
});
});

});
});
Loading
Loading