@sendbird/ai-agent-messenger-react 1.35.1 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cjs/B9JzuCyz.cjs +1 -0
- package/dist/cjs/BQ8EaWkc.cjs +1 -0
- package/dist/cjs/C116E0pr.cjs +1 -0
- package/dist/cjs/C2Oi-KA4.cjs +1 -0
- package/dist/cjs/C3VEshQr.cjs +1 -0
- package/dist/cjs/CIjjIGUs.cjs +1 -0
- package/dist/cjs/CKr98WbE.cjs +1 -0
- package/dist/cjs/Cr2LxvlJ.cjs +1 -0
- package/dist/cjs/LFX2h7v2.cjs +1 -0
- package/dist/es/{DbWklFH0.js → 0pQ1gLAM.js} +5 -4
- package/dist/es/{FMOCAKFw.js → 2cuzbJp5.js} +5 -4
- package/dist/es/{Dh05zfqw.js → Bz42IMvh.js} +5 -4
- package/dist/es/{hPxGrQQg.js → C6lvBtlQ.js} +3 -2
- package/dist/es/{BDCT28XM.js → CPxk5f9K.js} +1 -0
- package/dist/es/{CZXgaS-3.js → CrgvxcG1.js} +1 -0
- package/dist/es/{C5fUw0RP.js → DQJT1L-o.js} +1 -0
- package/dist/es/{RwjGyHFV.js → DYWGE-YD.js} +1 -0
- package/dist/es/{BWCZN9kg.js → yYj8P0Nc.js} +3 -2
- package/dist/index.cjs +104 -104
- package/dist/index.d.ts +1627 -902
- package/dist/index.js +5081 -4726
- package/package.json +4 -4
- package/dist/cjs/BMDJStMQ.cjs +0 -1
- package/dist/cjs/CLIpC7kY.cjs +0 -1
- package/dist/cjs/CPZf2TWM.cjs +0 -1
- package/dist/cjs/ChrM6HN1.cjs +0 -1
- package/dist/cjs/D7UQ9OK7.cjs +0 -1
- package/dist/cjs/DKQwAy8C.cjs +0 -1
- package/dist/cjs/D_km8dcd.cjs +0 -1
- package/dist/cjs/I-l-5Zo-.cjs +0 -1
- package/dist/cjs/etCQgYrr.cjs +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ import type { AIAgentModule } from '@sendbird/chat/aiAgent';
|
|
|
4
4
|
import { AriaRole } from 'react';
|
|
5
5
|
import type { BaseChannelCollectionEventHandler } from '@sendbird/chat/groupChannel';
|
|
6
6
|
import type { BaseMessage } from '@sendbird/chat/message';
|
|
7
|
-
import type { CollectionEventSource } from '@sendbird/chat';
|
|
8
7
|
import { ComponentType } from 'react';
|
|
9
8
|
import { Context } from 'react';
|
|
10
9
|
import type { Conversation as Conversation_2 } from '@sendbird/chat/aiAgent';
|
|
@@ -292,29 +291,28 @@ declare abstract class AIAgentBaseStats {
|
|
|
292
291
|
protected committed: boolean = false;
|
|
293
292
|
protected commitCallback: StatsAppendCallback | null = null;
|
|
294
293
|
protected conversationId: number | null = null;
|
|
295
|
-
protected channelUrl: string | null = null;
|
|
296
294
|
protected errorCode: number | null = null;
|
|
297
295
|
protected errorDescription: string | null = null;
|
|
298
296
|
protected extraData: Record<string, unknown> = {};
|
|
299
297
|
|
|
298
|
+
protected abstract buildPayload(): AIAgentStatPayload | null;
|
|
299
|
+
protected abstract getMetricKey(): string;
|
|
300
|
+
|
|
300
301
|
setCommitCallback(callback: StatsAppendCallback): this {
|
|
301
302
|
this.commitCallback = callback;
|
|
302
303
|
return this;
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
setConversationId(id
|
|
306
|
-
this.conversationId = id;
|
|
306
|
+
setConversationId(id?: number): this {
|
|
307
|
+
if (id) this.conversationId = id;
|
|
307
308
|
return this;
|
|
308
309
|
}
|
|
309
310
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return this;
|
|
313
|
-
}
|
|
311
|
+
setError(error: unknown | null | undefined): this {
|
|
312
|
+
if (error === null || error === undefined) return this;
|
|
314
313
|
|
|
315
|
-
|
|
316
|
-
this.
|
|
317
|
-
this.errorDescription = description;
|
|
314
|
+
this.errorCode = getErrorCode(error) ?? DEFAULT_ERROR_CODE;
|
|
315
|
+
this.errorDescription = getErrorDescription(error);
|
|
318
316
|
return this;
|
|
319
317
|
}
|
|
320
318
|
|
|
@@ -331,7 +329,7 @@ declare abstract class AIAgentBaseStats {
|
|
|
331
329
|
const existing = this.timers.get(key);
|
|
332
330
|
if (existing?.startTime !== null && existing?.startTime !== undefined) return this;
|
|
333
331
|
|
|
334
|
-
this.timers.set(key, { startTime:
|
|
332
|
+
this.timers.set(key, { startTime: performance.now(), endTime: null });
|
|
335
333
|
return this;
|
|
336
334
|
}
|
|
337
335
|
|
|
@@ -341,22 +339,51 @@ declare abstract class AIAgentBaseStats {
|
|
|
341
339
|
const existing = this.timers.get(key);
|
|
342
340
|
if (!existing || existing.startTime === null || existing.endTime !== null) return this;
|
|
343
341
|
|
|
344
|
-
this.timers.set(key, { ...existing, endTime:
|
|
342
|
+
this.timers.set(key, { ...existing, endTime: performance.now() });
|
|
345
343
|
return this;
|
|
346
344
|
}
|
|
347
345
|
|
|
348
|
-
|
|
346
|
+
getTimer(key: string): TimerReading {
|
|
349
347
|
const timer = this.timers.get(key);
|
|
350
|
-
if (!timer || timer.startTime === null
|
|
351
|
-
return
|
|
348
|
+
if (!timer || timer.startTime === null) return { startAt: null, endedAt: null, duration: null };
|
|
349
|
+
return {
|
|
350
|
+
startAt: timer.startTime,
|
|
351
|
+
endedAt: timer.endTime,
|
|
352
|
+
duration: timer.endTime === null ? null : timer.endTime - timer.startTime,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
protected createPayload(value: string | number, params?: StatsPayloadParams): AIAgentStatPayload {
|
|
357
|
+
const payload: AIAgentStatPayload = {
|
|
358
|
+
key: this.getMetricKey(),
|
|
359
|
+
value: String(value),
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const conversationId = params?.conversationId ?? this.conversationId ?? undefined;
|
|
363
|
+
if (conversationId !== undefined) payload.conversation_id = conversationId;
|
|
364
|
+
|
|
365
|
+
if (this.errorCode !== null) {
|
|
366
|
+
payload.error_code = this.errorCode;
|
|
367
|
+
payload.error_description = this.errorDescription ?? '';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const extra = dropNullish({
|
|
371
|
+
...this.extraData,
|
|
372
|
+
...params?.extra,
|
|
373
|
+
});
|
|
374
|
+
if (Object.keys(extra).length > 0) payload.extra = extra;
|
|
375
|
+
|
|
376
|
+
return payload;
|
|
352
377
|
}
|
|
353
378
|
|
|
354
379
|
isCommitted(): boolean {
|
|
355
380
|
return this.committed;
|
|
356
381
|
}
|
|
357
382
|
|
|
358
|
-
|
|
359
|
-
|
|
383
|
+
/** Consume without emitting: the metric counts as committed for once-per-session gating. */
|
|
384
|
+
discard(): void {
|
|
385
|
+
this.committed = true;
|
|
386
|
+
}
|
|
360
387
|
|
|
361
388
|
commit(): boolean {
|
|
362
389
|
if (this.committed || !this.commitCallback) {
|
|
@@ -364,14 +391,10 @@ declare abstract class AIAgentBaseStats {
|
|
|
364
391
|
}
|
|
365
392
|
|
|
366
393
|
const payload = this.buildPayload();
|
|
367
|
-
|
|
368
|
-
if (!payload) {
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
394
|
+
if (!payload) return false;
|
|
371
395
|
|
|
372
396
|
this.committed = true;
|
|
373
|
-
|
|
374
|
-
return result;
|
|
397
|
+
return this.commitCallback(AI_AGENT_STAT_TYPE, payload);
|
|
375
398
|
}
|
|
376
399
|
}
|
|
377
400
|
|
|
@@ -587,7 +610,6 @@ declare interface AIAgentConversationContextValue {
|
|
|
587
610
|
messages: ChatSDKBaseMessage[];
|
|
588
611
|
newMessages: ChatSDKBaseMessage[];
|
|
589
612
|
initialized: boolean;
|
|
590
|
-
subscribeMessageEvents: (listener: (event: ConversationMessageEvent) => void) => () => void;
|
|
591
613
|
loadPrevious: () => Promise<void>;
|
|
592
614
|
loadNext: () => Promise<void>;
|
|
593
615
|
sendUserMessage: (params: ChatSDKUserMessageCreateParams) => Promise<ChatSDKUserMessage>;
|
|
@@ -608,6 +630,8 @@ declare interface AIAgentConversationContextValue {
|
|
|
608
630
|
};
|
|
609
631
|
input: InputState;
|
|
610
632
|
};
|
|
633
|
+
|
|
634
|
+
_statsEnabled: boolean;
|
|
611
635
|
}
|
|
612
636
|
|
|
613
637
|
declare interface AIAgentConversationListContextValue {
|
|
@@ -728,6 +752,9 @@ declare interface AIAgentInterface {
|
|
|
728
752
|
/** @internal Schedules metrics work that should only start while the app is foregrounded. */
|
|
729
753
|
readonly scheduleForegroundTask?: ForegroundTaskScheduler;
|
|
730
754
|
|
|
755
|
+
/** @internal Watches whether the app was backgrounded during a stats measurement window. */
|
|
756
|
+
readonly watchBackgrounded?: BackgroundedWatcher;
|
|
757
|
+
|
|
731
758
|
/** @internal Stats trackers owned by this SDK instance. */
|
|
732
759
|
readonly statsTrackers: AIAgentStatsTrackers;
|
|
733
760
|
|
|
@@ -749,6 +776,8 @@ declare interface AIAgentInterface {
|
|
|
749
776
|
updateContext: (channelUrl: string, context: Record<string, string>) => Promise<ContextObject>;
|
|
750
777
|
/** Retrieves the context data for the specified channel */
|
|
751
778
|
getContextObject: (channelUrl: string) => Promise<ContextObject>;
|
|
779
|
+
/** Requests a nudge message to be shown above the launcher. Fire-and-forget: the result is only broadcast to onNudgeMessageReceived subscribers */
|
|
780
|
+
requestNudgeMessage: (params: RequestNudgeMessageParams) => Promise<void>;
|
|
752
781
|
/** Clears memories saved for the current user */
|
|
753
782
|
clearUserMemories: (params?: ClearUserMemoriesParams) => Promise<void>;
|
|
754
783
|
|
|
@@ -759,7 +788,7 @@ declare interface AIAgentInterface {
|
|
|
759
788
|
setUserMemoryState: (params: SetUserMemoryStateParams) => Promise<MemoryInfo>;
|
|
760
789
|
|
|
761
790
|
/** @internal Initializes a conversation on the channel if needed (AI Agent channel without conversation). */
|
|
762
|
-
initConversationIfNeeded: (channel: ChatSDKGroupChannel) => Promise<void>;
|
|
791
|
+
initConversationIfNeeded: (channel: ChatSDKGroupChannel, params?: { initialUserMessage?: string }) => Promise<void>;
|
|
763
792
|
|
|
764
793
|
/** @internal Live metric handler; bridged from {@link AIAgentClientHandlers.onLiveMetric}. */
|
|
765
794
|
onLiveMetric?: LiveMetricDispatch;
|
|
@@ -770,6 +799,8 @@ declare interface AIAgentInterface {
|
|
|
770
799
|
/** Disposes all resources held by this instance (WebSocket, desk auth, event handlers). */
|
|
771
800
|
dispose: () => void;
|
|
772
801
|
|
|
802
|
+
/** Registers a handler for nudge message responses */
|
|
803
|
+
onNudgeMessageReceived: (handler: (response: NudgeMessageResponse) => void) => () => void;
|
|
773
804
|
/** Registers a handler for messenger settings updates */
|
|
774
805
|
onMessengerSettingsUpdated: (handler: (params: MessengerSettingsUpdatedParams) => void) => () => void;
|
|
775
806
|
/** Registers a handler for active channel updates */
|
|
@@ -830,6 +861,8 @@ declare interface AIAgentMessengerSessionRef {
|
|
|
830
861
|
/** Patches the context of a specific conversation by aiAgentId and channelUrl. */
|
|
831
862
|
(aiAgentId: string, channelUrl: string, context: Record<string, string>): Promise<ContextObject>;
|
|
832
863
|
};
|
|
864
|
+
/** Requests a nudge message to be shown above the launcher. Fire-and-forget: the response is not exposed. */
|
|
865
|
+
requestNudgeMessage: (params: RequestNudgeMessageParams) => Promise<void>;
|
|
833
866
|
}
|
|
834
867
|
|
|
835
868
|
export declare type AIAgentProps = PropsWithChildren<{
|
|
@@ -1136,6 +1169,7 @@ declare interface AIAgentStringSet {
|
|
|
1136
1169
|
a11y_send_message: string;
|
|
1137
1170
|
a11y_attach_file: string;
|
|
1138
1171
|
a11y_close_messenger: string;
|
|
1172
|
+
a11y_dismiss_nudge_message: string;
|
|
1139
1173
|
a11y_expand_messenger: string;
|
|
1140
1174
|
a11y_collapse_messenger: string;
|
|
1141
1175
|
a11y_connect_agent: string;
|
|
@@ -1278,23 +1312,40 @@ declare interface AIAgentStringSet {
|
|
|
1278
1312
|
};
|
|
1279
1313
|
}
|
|
1280
1314
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1315
|
+
/**
|
|
1316
|
+
* Spec: aiev_auth_expired_unhandled
|
|
1317
|
+
*
|
|
1318
|
+
* Records a server EXTERNAL_AUTH_TOKEN_EXPIRED event that the SDK cannot
|
|
1319
|
+
* deliver to the host app.
|
|
1320
|
+
*
|
|
1321
|
+
* Timeline:
|
|
1322
|
+
* - Trigger when the SDK receives the external-token-expired AIEV payload.
|
|
1323
|
+
* - Commit immediately if payload parsing fails or no host callback/delegate
|
|
1324
|
+
* can receive the event.
|
|
1325
|
+
*
|
|
1326
|
+
* Payload:
|
|
1327
|
+
* - value: failure_reason = payload_parse_failed | callback_not_found.
|
|
1328
|
+
* - extra.agent_id: AI Agent id when available.
|
|
1329
|
+
*
|
|
1330
|
+
* Policy:
|
|
1331
|
+
* - Fire-and-forget; one failed event produces one stat.
|
|
1332
|
+
* - Successful delivery to the host app is not recorded by this metric.
|
|
1333
|
+
*/
|
|
1334
|
+
declare class AIEVAuthExpiredUnhandledStatsTracker extends StatsTrackerBase {
|
|
1288
1335
|
onPayloadParseFailed(params: { aiAgentId: string }): void {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1336
|
+
new AIEVAuthExpiredUnhandledStats()
|
|
1337
|
+
.setReason('payload_parse_failed')
|
|
1338
|
+
.setAIAgentId(params.aiAgentId)
|
|
1339
|
+
.setCommitCallback(this.commitCallback)
|
|
1340
|
+
.commit();
|
|
1292
1341
|
}
|
|
1293
1342
|
|
|
1294
1343
|
onCallbackNotFound(params: { aiAgentId: string }): void {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1344
|
+
new AIEVAuthExpiredUnhandledStats()
|
|
1345
|
+
.setReason('callback_not_found')
|
|
1346
|
+
.setAIAgentId(params.aiAgentId)
|
|
1347
|
+
.setCommitCallback(this.commitCallback)
|
|
1348
|
+
.commit();
|
|
1298
1349
|
}
|
|
1299
1350
|
|
|
1300
1351
|
clear(): void {}
|
|
@@ -1344,8 +1395,15 @@ declare interface AttachmentRules {
|
|
|
1344
1395
|
getUploadSizeLimitInMB: (mimeType: string) => number;
|
|
1345
1396
|
}
|
|
1346
1397
|
|
|
1398
|
+
declare type AuthState = 'idle' | 'in_progress' | 'completed' | 'failed';
|
|
1399
|
+
|
|
1400
|
+
declare type AuthState_2 = 'idle' | 'in_progress' | 'completed' | 'failed';
|
|
1401
|
+
|
|
1347
1402
|
declare interface AuthTokenRefreshedParams {}
|
|
1348
1403
|
|
|
1404
|
+
/** Starts watching foreground state; the returned function stops watching and reports whether the app was backgrounded while watched. */
|
|
1405
|
+
declare type BackgroundedWatcher = () => () => boolean;
|
|
1406
|
+
|
|
1349
1407
|
declare abstract class BaseCommand {
|
|
1350
1408
|
abstract type: CommandType;
|
|
1351
1409
|
payload: any;
|
|
@@ -1389,7 +1447,7 @@ declare type CachedMessengerSettings = Omit<MessengerSettingsResponse, 'memory'>
|
|
|
1389
1447
|
export declare type ChallengeActionParams = {
|
|
1390
1448
|
key: string;
|
|
1391
1449
|
requestId: string;
|
|
1392
|
-
action: 'submit' | 'cancel';
|
|
1450
|
+
action: 'submit' | 'cancel' | 'fail';
|
|
1393
1451
|
data?: Record<string, unknown>;
|
|
1394
1452
|
};
|
|
1395
1453
|
|
|
@@ -1407,8 +1465,6 @@ declare type ChatSDKBaseChannelCollectionEventHandler = BaseChannelCollectionEve
|
|
|
1407
1465
|
|
|
1408
1466
|
declare type ChatSDKBaseMessage = BaseMessage;
|
|
1409
1467
|
|
|
1410
|
-
declare type ChatSDKCollectionEventSource = CollectionEventSource;
|
|
1411
|
-
|
|
1412
1468
|
declare type ChatSDKConversation = Conversation_2;
|
|
1413
1469
|
|
|
1414
1470
|
declare type ChatSDKConversationStatus = ConversationStatus;
|
|
@@ -1491,6 +1547,9 @@ declare interface ColorSet {
|
|
|
1491
1547
|
csat: {
|
|
1492
1548
|
background: string;
|
|
1493
1549
|
};
|
|
1550
|
+
conversation: {
|
|
1551
|
+
headerProfileSkeleton: string;
|
|
1552
|
+
};
|
|
1494
1553
|
memoryDialog: {
|
|
1495
1554
|
titleText: string;
|
|
1496
1555
|
markdownBodyText: string;
|
|
@@ -1529,6 +1588,7 @@ declare interface CommandPayloads {
|
|
|
1529
1588
|
[CommandType.ActiveChannelUpdated]: ActiveChannelUpdatedParams;
|
|
1530
1589
|
[CommandType.AnonymousSessionTokenExpired]: undefined;
|
|
1531
1590
|
[CommandType.AuthTokenRefreshed]: AuthTokenRefreshedParams;
|
|
1591
|
+
[CommandType.NudgeMessageReceived]: NudgeMessageResponse;
|
|
1532
1592
|
}
|
|
1533
1593
|
|
|
1534
1594
|
export declare const Commands = {
|
|
@@ -1537,6 +1597,7 @@ export declare const Commands = {
|
|
|
1537
1597
|
ActiveChannelUpdated: (params: ActiveChannelUpdatedParams) => new ActiveChannelUpdatedCommand(params),
|
|
1538
1598
|
AnonymousSessionTokenExpired: () => new AnonymousSessionTokenExpiredCommand(),
|
|
1539
1599
|
AuthTokenRefreshed: () => new AuthTokenRefreshedCommand(),
|
|
1600
|
+
NudgeMessageReceived: (response: NudgeMessageResponse) => new NudgeMessageReceivedCommand(response),
|
|
1540
1601
|
};
|
|
1541
1602
|
|
|
1542
1603
|
declare enum CommandType {
|
|
@@ -1545,6 +1606,7 @@ declare enum CommandType {
|
|
|
1545
1606
|
ActiveChannelUpdated = 'active.channel.updated',
|
|
1546
1607
|
AnonymousSessionTokenExpired = 'anon.token.expired',
|
|
1547
1608
|
AuthTokenRefreshed = 'auth.token.refreshed',
|
|
1609
|
+
NudgeMessageReceived = 'nudge.message.received',
|
|
1548
1610
|
}
|
|
1549
1611
|
|
|
1550
1612
|
export declare interface ConnectionDelayDialogProps {
|
|
@@ -1590,7 +1652,7 @@ declare interface ContextObject {
|
|
|
1590
1652
|
context: Record<string, string>;
|
|
1591
1653
|
}
|
|
1592
1654
|
|
|
1593
|
-
export declare const Conversation: ({ children, onNavigateToConversationList, channelUrl, onClearChannelUrl, shouldMarkAsRead, shouldInitializeConversation, announcementsEnabled, conversationStatsEnabled, initialFocusTarget, style, closedChannelUrl, onClearClosedChannelUrl, }: ConversationProps) => JSX.Element;
|
|
1655
|
+
export declare const Conversation: ({ children, onNavigateToConversationList, channelUrl, onClearChannelUrl, shouldMarkAsRead, shouldInitializeConversation, initialUserMessage, announcementsEnabled, conversationStatsEnabled, initialFocusTarget, style, closedChannelUrl, onClearClosedChannelUrl, }: ConversationProps) => JSX.Element;
|
|
1594
1656
|
|
|
1595
1657
|
export declare const ConversationContext: Context<ConversationContextValue | null>;
|
|
1596
1658
|
|
|
@@ -1602,12 +1664,12 @@ declare interface ConversationContextProps {
|
|
|
1602
1664
|
conversationStatsEnabled?: boolean;
|
|
1603
1665
|
/** @internal */
|
|
1604
1666
|
shouldInitializeConversation?: boolean;
|
|
1667
|
+
initialUserMessage?: string;
|
|
1605
1668
|
}
|
|
1606
1669
|
|
|
1607
1670
|
export declare const ConversationContextProvider: ({ children, ...props }: PropsWithChildren<ConversationContextProps>) => JSX.Element;
|
|
1608
1671
|
|
|
1609
1672
|
declare interface ConversationContextValue extends AIAgentConversationContextValue {
|
|
1610
|
-
conversationStatsEnabled: boolean;
|
|
1611
1673
|
scrollSource: ConversationScrollContextValue;
|
|
1612
1674
|
goToActiveConversation: () => Promise<void>;
|
|
1613
1675
|
onNavigateToConversationList?: () => void;
|
|
@@ -1704,234 +1766,326 @@ declare interface ConversationHeaderTemplateProps {
|
|
|
1704
1766
|
|
|
1705
1767
|
declare type ConversationInitialFocusTarget = 'messageInput';
|
|
1706
1768
|
|
|
1769
|
+
declare type ConversationInitialRenderMeasure = {
|
|
1770
|
+
channelUrl: string;
|
|
1771
|
+
pageRenderState: PageRenderState;
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1707
1774
|
declare class ConversationInitialRenderStats extends AIAgentBaseStats {
|
|
1775
|
+
private channelUrl: string | null = null;
|
|
1708
1776
|
private presentMethod: PresentMethod = 'direct_present';
|
|
1709
1777
|
|
|
1778
|
+
setChannelUrl(url: string): this {
|
|
1779
|
+
this.channelUrl = url;
|
|
1780
|
+
return this;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1710
1783
|
setPresentMethod(method: PresentMethod): this {
|
|
1711
1784
|
this.presentMethod = method;
|
|
1712
1785
|
return this;
|
|
1713
1786
|
}
|
|
1714
1787
|
|
|
1788
|
+
resetPageRenderSpan(): this {
|
|
1789
|
+
this.timers.delete(DurationKey.PAGE_RENDER);
|
|
1790
|
+
this.timers.delete(DurationKey.PAGE_RENDER_FROM_CACHE);
|
|
1791
|
+
this.timers.delete(DurationKey.GET_CHANNEL);
|
|
1792
|
+
this.timers.delete(DurationKey.GET_MESSAGES);
|
|
1793
|
+
this.conversationId = null;
|
|
1794
|
+
this.channelUrl = null;
|
|
1795
|
+
this.errorCode = null;
|
|
1796
|
+
this.errorDescription = null;
|
|
1797
|
+
return this;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1715
1800
|
protected getMetricKey(): string {
|
|
1716
1801
|
return METRIC_KEY_CONVERSATION_INITIAL_RENDER;
|
|
1717
1802
|
}
|
|
1718
1803
|
|
|
1719
1804
|
protected buildPayload(): AIAgentStatPayload | null {
|
|
1720
|
-
const
|
|
1721
|
-
const
|
|
1722
|
-
const
|
|
1723
|
-
const totalDurationFromCache = this.getDuration(DurationKey.TOTAL_DURATION_FROM_CACHE);
|
|
1724
|
-
const totalDuration = this.getDuration(DurationKey.TOTAL_DURATION);
|
|
1805
|
+
const auth = this.getTimer(DurationKey.AUTH);
|
|
1806
|
+
const pageRender = this.getTimer(DurationKey.PAGE_RENDER);
|
|
1807
|
+
const fromCache = this.getTimer(DurationKey.PAGE_RENDER_FROM_CACHE);
|
|
1725
1808
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
};
|
|
1809
|
+
// Only the auth-error early commit legitimately lacks a page render duration;
|
|
1810
|
+
// any other commit without it is an incomplete measurement and must not emit.
|
|
1811
|
+
if (pageRender.duration === null && (this.errorCode === null || auth.duration === null)) return null;
|
|
1812
|
+
|
|
1813
|
+
// The timers race freely; subtracting the overlap counts the auth wait exactly once.
|
|
1814
|
+
const pageRenderDuration = pageRender.duration === null ? 0 : pageRender.duration - overlapMs(auth, pageRender);
|
|
1815
|
+
const fromCacheDuration = fromCache.duration === null ? null : fromCache.duration - overlapMs(auth, fromCache);
|
|
1734
1816
|
|
|
1735
|
-
return
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1817
|
+
return this.createPayload((auth.duration ?? 0) + pageRenderDuration, {
|
|
1818
|
+
extra: {
|
|
1819
|
+
channel_url: this.channelUrl,
|
|
1820
|
+
present_method: this.presentMethod,
|
|
1821
|
+
[DurationKey.AUTH]: auth.duration,
|
|
1822
|
+
[DurationKey.GET_CHANNEL]: this.getTimer(DurationKey.GET_CHANNEL).duration,
|
|
1823
|
+
[DurationKey.GET_MESSAGES]: this.getTimer(DurationKey.GET_MESSAGES).duration,
|
|
1824
|
+
[DurationKey.PAGE_RENDER_FROM_CACHE]: fromCacheDuration,
|
|
1825
|
+
},
|
|
1739
1826
|
});
|
|
1740
1827
|
}
|
|
1741
1828
|
}
|
|
1742
1829
|
|
|
1743
1830
|
/**
|
|
1744
|
-
*
|
|
1831
|
+
* One Conversation entry attempt. Owns the page render phase machine; the tracker
|
|
1832
|
+
* owns the cross-attempt policy (auth span, supersession, one commit per entry).
|
|
1833
|
+
*
|
|
1834
|
+
* Async-boundary events carry the channel identity so a result delivered to the
|
|
1835
|
+
* wrong span is rejected instead of consuming a phase transition.
|
|
1836
|
+
*
|
|
1837
|
+
* Timers record their own event boundaries and race freely against auth; the
|
|
1838
|
+
* metric subtracts the overlap at payload time, so event ordering needs no
|
|
1839
|
+
* orchestration here.
|
|
1745
1840
|
*/
|
|
1746
|
-
declare class
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
private consumedByAuthError = false;
|
|
1752
|
-
private presentMethod: PresentMethod = 'direct_present';
|
|
1753
|
-
private renderedAfterApiResult = false;
|
|
1754
|
-
private stats: ConversationInitialRenderStats | null = null;
|
|
1841
|
+
declare class ConversationInitialRenderStatsSpan extends StatsSpanBase<
|
|
1842
|
+
ConversationInitialRenderStats,
|
|
1843
|
+
ConversationInitialRenderMeasure
|
|
1844
|
+
> {
|
|
1845
|
+
protected measure: ConversationInitialRenderMeasure;
|
|
1755
1846
|
|
|
1756
|
-
constructor(
|
|
1757
|
-
|
|
1847
|
+
constructor(stats: ConversationInitialRenderStats | null, channelUrl: string, visible: boolean) {
|
|
1848
|
+
super(stats, visible);
|
|
1849
|
+
this.measure = { channelUrl, pageRenderState: 'initialized' };
|
|
1850
|
+
this.stats
|
|
1851
|
+
?.setChannelUrl(channelUrl)
|
|
1852
|
+
.startTimer(DurationKey.PAGE_RENDER)
|
|
1853
|
+
.startTimer(DurationKey.PAGE_RENDER_FROM_CACHE);
|
|
1758
1854
|
}
|
|
1759
1855
|
|
|
1760
|
-
|
|
1761
|
-
if (this.
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
}
|
|
1766
|
-
if (!this.stats) {
|
|
1767
|
-
this.stats = new ConversationInitialRenderStats()
|
|
1768
|
-
.setCommitCallback(this.commitCallback)
|
|
1769
|
-
.setPresentMethod(this.presentMethod);
|
|
1770
|
-
this.apiResultReceived = false;
|
|
1771
|
-
this.renderedAfterApiResult = false;
|
|
1772
|
-
}
|
|
1773
|
-
return this.stats;
|
|
1856
|
+
onGetChannelStart(channelUrl: string): void {
|
|
1857
|
+
if (!this.stats || channelUrl !== this.measure.channelUrl || this.measure.pageRenderState !== 'initialized') return;
|
|
1858
|
+
|
|
1859
|
+
this.stats.startTimer(DurationKey.GET_CHANNEL);
|
|
1860
|
+
this.measure.pageRenderState = 'channel_fetching';
|
|
1774
1861
|
}
|
|
1775
1862
|
|
|
1776
|
-
|
|
1777
|
-
if (this.
|
|
1778
|
-
return
|
|
1863
|
+
onGetChannelComplete(channelUrl: string, conversationId?: number): void {
|
|
1864
|
+
if (!this.stats || channelUrl !== this.measure.channelUrl || this.measure.pageRenderState !== 'channel_fetching')
|
|
1865
|
+
return;
|
|
1866
|
+
|
|
1867
|
+
this.stats.stopTimer(DurationKey.GET_CHANNEL).setConversationId(conversationId);
|
|
1868
|
+
this.measure.pageRenderState = 'channel_fetched';
|
|
1779
1869
|
}
|
|
1780
1870
|
|
|
1781
|
-
|
|
1782
|
-
|
|
1871
|
+
onGetChannelError(channelUrl: string, error: Error): void {
|
|
1872
|
+
if (!this.stats || channelUrl !== this.measure.channelUrl || this.measure.pageRenderState !== 'channel_fetching')
|
|
1873
|
+
return;
|
|
1874
|
+
|
|
1875
|
+
this.stats.stopTimer(DurationKey.GET_CHANNEL).setError(error).stopTimer(DurationKey.PAGE_RENDER);
|
|
1876
|
+
this.measure.pageRenderState = 'channel_failed';
|
|
1877
|
+
this.tryFinalize();
|
|
1783
1878
|
}
|
|
1784
1879
|
|
|
1785
|
-
|
|
1786
|
-
if (
|
|
1787
|
-
if (!this.matchesChannelUrl(channelUrl)) return false;
|
|
1880
|
+
onGetMessagesStart(): void {
|
|
1881
|
+
if (!this.stats || this.measure.pageRenderState !== 'channel_fetched') return;
|
|
1788
1882
|
|
|
1789
|
-
this.
|
|
1790
|
-
this.
|
|
1791
|
-
return true;
|
|
1883
|
+
this.stats.startTimer(DurationKey.GET_MESSAGES);
|
|
1884
|
+
this.measure.pageRenderState = 'messages_fetching';
|
|
1792
1885
|
}
|
|
1793
1886
|
|
|
1794
|
-
|
|
1795
|
-
if (
|
|
1796
|
-
|
|
1887
|
+
onGetMessagesCacheComplete(channelUrl: string, error: Error | null): void {
|
|
1888
|
+
if (
|
|
1889
|
+
!this.stats ||
|
|
1890
|
+
channelUrl !== this.measure.channelUrl ||
|
|
1891
|
+
error ||
|
|
1892
|
+
this.measure.pageRenderState !== 'messages_fetching'
|
|
1893
|
+
) {
|
|
1894
|
+
return;
|
|
1797
1895
|
}
|
|
1798
|
-
return commitResult;
|
|
1799
|
-
}
|
|
1800
1896
|
|
|
1801
|
-
|
|
1802
|
-
if (this.consumed || !this.stats || this.stats.isCommitted()) return true;
|
|
1803
|
-
if (!this.matchesChannelUrl(channelUrl)) return false;
|
|
1804
|
-
if (!this.apiResultReceived || !this.renderedAfterApiResult) return false;
|
|
1805
|
-
return this.consume(this.stats.stopTimer(DurationKey.TOTAL_DURATION).commit());
|
|
1897
|
+
this.stats.stopTimer(DurationKey.PAGE_RENDER_FROM_CACHE);
|
|
1806
1898
|
}
|
|
1807
1899
|
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1900
|
+
onGetMessagesComplete(channelUrl: string, error: Error | null): void {
|
|
1901
|
+
if (!this.stats || channelUrl !== this.measure.channelUrl || this.measure.pageRenderState !== 'messages_fetching')
|
|
1902
|
+
return;
|
|
1811
1903
|
|
|
1812
|
-
stats.
|
|
1904
|
+
this.stats.stopTimer(DurationKey.GET_MESSAGES).setError(error);
|
|
1905
|
+
this.measure.pageRenderState = 'messages_fetching_completed';
|
|
1813
1906
|
}
|
|
1814
1907
|
|
|
1815
|
-
|
|
1816
|
-
if (this.
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
this.
|
|
1908
|
+
onRendered(): void {
|
|
1909
|
+
if (!this.stats || this.measure.pageRenderState !== 'messages_fetching_completed') return;
|
|
1910
|
+
|
|
1911
|
+
this.stats.stopTimer(DurationKey.PAGE_RENDER);
|
|
1912
|
+
this.tryFinalize();
|
|
1820
1913
|
}
|
|
1821
1914
|
|
|
1822
|
-
|
|
1823
|
-
this.
|
|
1915
|
+
onRenderDiscarded(): void {
|
|
1916
|
+
if (!this.stats || this.measure.pageRenderState !== 'messages_fetching_completed') return;
|
|
1917
|
+
|
|
1918
|
+
this.stats.discard();
|
|
1919
|
+
this.stats = null;
|
|
1824
1920
|
}
|
|
1825
1921
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
const committed = this.consume(
|
|
1830
|
-
stats
|
|
1831
|
-
.stopTimer(DurationKey.AUTH)
|
|
1832
|
-
.setError(error instanceof SendbirdError ? error.code : undefined, error.message)
|
|
1833
|
-
.stopTimer(DurationKey.TOTAL_DURATION)
|
|
1834
|
-
.commit(),
|
|
1835
|
-
);
|
|
1836
|
-
if (committed) {
|
|
1837
|
-
this.consumedByAuthError = true;
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1922
|
+
/** Auth settled while this span was measuring; a finalize that waited on in-flight auth can proceed. */
|
|
1923
|
+
onAuthSettled(): void {
|
|
1924
|
+
this.tryFinalize();
|
|
1840
1925
|
}
|
|
1841
1926
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
if (!
|
|
1927
|
+
/** Commit once both facts exist: the page render ended (render or failure) and auth is not in flight. */
|
|
1928
|
+
private tryFinalize(): void {
|
|
1929
|
+
if (!this.stats) return;
|
|
1930
|
+
if (this.stats.getTimer(DurationKey.PAGE_RENDER).endedAt === null) return;
|
|
1845
1931
|
|
|
1846
|
-
stats.
|
|
1932
|
+
const auth = this.stats.getTimer(DurationKey.AUTH);
|
|
1933
|
+
if (auth.startAt !== null && auth.endedAt === null) return;
|
|
1934
|
+
|
|
1935
|
+
if (this.measure.pageRenderState === 'channel_failed') {
|
|
1936
|
+
// Early error commit (iOS/Android convergence) bypasses the visibility gate, like onAuthError.
|
|
1937
|
+
this.stats.commit();
|
|
1938
|
+
this.stats = null;
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
this.commitOrDefer();
|
|
1847
1942
|
}
|
|
1943
|
+
}
|
|
1848
1944
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1945
|
+
/**
|
|
1946
|
+
* Spec: conversation_initial_render
|
|
1947
|
+
*
|
|
1948
|
+
* Measures the first Conversation screen render as the sum of auth and the
|
|
1949
|
+
* page render span required before the first remote-rendered message list frame.
|
|
1950
|
+
*
|
|
1951
|
+
* Timeline:
|
|
1952
|
+
* - Resolve present method.
|
|
1953
|
+
* - Track auth independently; it can start before any Conversation screen exists.
|
|
1954
|
+
* - Start a span when the Conversation screen display lifecycle begins.
|
|
1955
|
+
* - Track get-channel and get-messages phases on the span.
|
|
1956
|
+
* - Capture cache completion as best-effort total_duration_from_cache_ms, but
|
|
1957
|
+
* do not commit until the remote render path completes.
|
|
1958
|
+
* - Commit on the first remote rendered frame. While the screen is hidden the
|
|
1959
|
+
* measurement still freezes at that frame, but emission is deferred until the
|
|
1960
|
+
* screen becomes visible.
|
|
1961
|
+
* - Discard (no payload) when the rendered frame was deferred by a
|
|
1962
|
+
* backgrounded page/app; the session stays consumed.
|
|
1963
|
+
* - Confluence/iOS/Android converge on early error commit for auth or
|
|
1964
|
+
* get-channel failures that prevent Conversation entry.
|
|
1965
|
+
* - Cache message-list failure is not terminal; remote message-list failure
|
|
1966
|
+
* stores error data, and the rendered signal commits the initial render attempt.
|
|
1967
|
+
*
|
|
1968
|
+
* Payload:
|
|
1969
|
+
* - value: total_duration_ms.
|
|
1970
|
+
* - extra.auth_duration_ms, get_channel_duration_ms, get_messages_duration_ms.
|
|
1971
|
+
* - extra.total_duration_from_cache_ms, present_method, channel_url.
|
|
1972
|
+
*
|
|
1973
|
+
* Method call order:
|
|
1974
|
+
* - Resolve present method - setPresentMethod()
|
|
1975
|
+
* - Start auth - onAuthStart()
|
|
1976
|
+
* - Complete auth - onAuthComplete()
|
|
1977
|
+
* - Fail auth - onAuthError()
|
|
1978
|
+
* - Enter display lifecycle - startSpan(channelUrl)
|
|
1979
|
+
* - Screen visibility change - setVisible(visible)
|
|
1980
|
+
* - Start get channel - span.onGetChannelStart(channelUrl)
|
|
1981
|
+
* - Complete get channel - span.onGetChannelComplete(channelUrl, conversationId)
|
|
1982
|
+
* - Fail get channel - span.onGetChannelError(channelUrl, error)
|
|
1983
|
+
* - Start remote message list - span.onGetMessagesStart()
|
|
1984
|
+
* - Complete cache message list - span.onGetMessagesCacheComplete(channelUrl, error)
|
|
1985
|
+
* - Complete remote message list - span.onGetMessagesComplete(channelUrl, error)
|
|
1986
|
+
* - Render conversation - span.onRendered()
|
|
1987
|
+
* - Render deferred by background - span.onRenderDiscarded()
|
|
1988
|
+
* - Abandon entry attempt - span.cancel()
|
|
1989
|
+
* - Logout (deauthenticate/dispose) - clear()
|
|
1990
|
+
*
|
|
1991
|
+
* Policy:
|
|
1992
|
+
* - Session-scoped; collect at most once per authenticated session. Only the
|
|
1993
|
+
* logout-time clear() reopens measurement: re-entering a conversation after
|
|
1994
|
+
* a commit is a warm navigation and is deliberately not re-measured.
|
|
1995
|
+
* - value = auth + (page render − overlap): the timers record their own event
|
|
1996
|
+
* boundaries and race freely, and the metric subtracts the overlapping
|
|
1997
|
+
* interval at payload time, so the auth wait is counted exactly once in any
|
|
1998
|
+
* ordering — a render, cache completion, or channel failure landing mid-auth
|
|
1999
|
+
* included; the cache extra derives the same way. The commit waits for
|
|
2000
|
+
* in-flight auth to settle, since the value needs the auth duration.
|
|
2001
|
+
* - startSpan supersedes the previous span: the old span becomes non-recording
|
|
2002
|
+
* and the page render phase restarts. Auth data carries over to the new span.
|
|
2003
|
+
* - After success or error commit, new spans are born non-recording until clear().
|
|
2004
|
+
* - An auth failure before any span started allows a fresh measurement on retry.
|
|
2005
|
+
* - A backgrounded page/app pauses requestAnimationFrame, so a render signal
|
|
2006
|
+
* arriving after background time would fold the idle dwell into
|
|
2007
|
+
* total_duration_ms. Such samples are discarded without a payload
|
|
2008
|
+
* (web-vitals convention) and consume the session like a commit.
|
|
2009
|
+
* - Measurement is independent of screen visibility: a hidden (launcher-closed
|
|
2010
|
+
* or route-inactive) preload keeps measuring with the same render criterion,
|
|
2011
|
+
* so visibility toggles never tear a measurement. Visibility only gates
|
|
2012
|
+
* emission: a render completed while hidden commits the moment the screen
|
|
2013
|
+
* becomes visible (setVisible), and a preload that is never shown is
|
|
2014
|
+
* cancelled with its span and never emitted.
|
|
2015
|
+
*/
|
|
2016
|
+
declare class ConversationInitialRenderStatsTracker extends SpanStatsTrackerBase<ConversationInitialRenderStatsSpan> {
|
|
2017
|
+
private stats: ConversationInitialRenderStats | null = null;
|
|
2018
|
+
private authState: AuthState = 'idle';
|
|
2019
|
+
private presentMethod: PresentMethod = 'direct_present';
|
|
1854
2020
|
|
|
1855
|
-
|
|
1856
|
-
if (
|
|
1857
|
-
|
|
2021
|
+
private getOrCreateStats(): ConversationInitialRenderStats | null {
|
|
2022
|
+
if (this.stats?.isCommitted()) return null;
|
|
2023
|
+
if (!this.stats) {
|
|
2024
|
+
this.stats = new ConversationInitialRenderStats()
|
|
2025
|
+
.setCommitCallback(this.commitCallback)
|
|
2026
|
+
.setPresentMethod(this.presentMethod);
|
|
1858
2027
|
}
|
|
1859
|
-
stats
|
|
2028
|
+
return this.stats;
|
|
1860
2029
|
}
|
|
1861
2030
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
const stats = this.getActiveStats();
|
|
1866
|
-
if (stats && this.pinChannelUrl(channelUrl)) {
|
|
1867
|
-
this.consume(
|
|
1868
|
-
stats
|
|
1869
|
-
.stopTimer(DurationKey.GET_CHANNEL)
|
|
1870
|
-
.setError(error instanceof SendbirdError ? error.code : undefined, error.message)
|
|
1871
|
-
.stopTimer(DurationKey.TOTAL_DURATION)
|
|
1872
|
-
.commit(),
|
|
1873
|
-
);
|
|
1874
|
-
}
|
|
2031
|
+
private getActiveStats(): ConversationInitialRenderStats | null {
|
|
2032
|
+
if (!this.stats || this.stats.isCommitted()) return null;
|
|
2033
|
+
return this.stats;
|
|
1875
2034
|
}
|
|
1876
2035
|
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
if (error) {
|
|
1881
|
-
stats.setError(error instanceof SendbirdError ? error.code : undefined, error.message);
|
|
1882
|
-
}
|
|
1883
|
-
stats.stopTimer(DurationKey.TOTAL_DURATION_FROM_CACHE);
|
|
1884
|
-
}
|
|
2036
|
+
setPresentMethod(method: PresentMethod): void {
|
|
2037
|
+
this.presentMethod = method;
|
|
2038
|
+
this.getActiveStats()?.setPresentMethod(method);
|
|
1885
2039
|
}
|
|
1886
2040
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
.setError(error instanceof SendbirdError ? error.code : undefined, error.message)
|
|
1894
|
-
.stopTimer(DurationKey.TOTAL_DURATION)
|
|
1895
|
-
.stopTimer(DurationKey.GET_MESSAGES)
|
|
1896
|
-
.commit(),
|
|
1897
|
-
);
|
|
1898
|
-
return;
|
|
1899
|
-
}
|
|
1900
|
-
this.apiResultReceived = true;
|
|
1901
|
-
this.renderedAfterApiResult = false;
|
|
1902
|
-
stats.stopTimer(DurationKey.GET_MESSAGES);
|
|
1903
|
-
this.commitIfReady();
|
|
2041
|
+
onAuthStart(): void {
|
|
2042
|
+
if (this.stats?.isCommitted()) {
|
|
2043
|
+
if (this.authState !== 'failed' || this.activeSpan !== null) return;
|
|
2044
|
+
// Auth failed before any Conversation entry: retrying auth starts a new measurement.
|
|
2045
|
+
this.stats = null;
|
|
2046
|
+
this.authState = 'idle';
|
|
1904
2047
|
}
|
|
1905
|
-
|
|
2048
|
+
if (this.authState !== 'idle') return;
|
|
1906
2049
|
|
|
1907
|
-
|
|
1908
|
-
this.
|
|
2050
|
+
this.authState = 'in_progress';
|
|
2051
|
+
this.getOrCreateStats()?.startTimer(DurationKey.AUTH);
|
|
1909
2052
|
}
|
|
1910
2053
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2054
|
+
onAuthComplete(): void {
|
|
2055
|
+
if (this.authState === 'in_progress') {
|
|
2056
|
+
this.authState = 'completed';
|
|
2057
|
+
this.getActiveStats()?.stopTimer(DurationKey.AUTH);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
this.activeSpan?.onAuthSettled();
|
|
1914
2061
|
}
|
|
1915
2062
|
|
|
1916
|
-
|
|
1917
|
-
if (
|
|
2063
|
+
onAuthError(error: Error): void {
|
|
2064
|
+
if (this.authState !== 'in_progress') return;
|
|
1918
2065
|
|
|
1919
|
-
this.
|
|
1920
|
-
|
|
2066
|
+
this.authState = 'failed';
|
|
2067
|
+
this.getActiveStats()?.stopTimer(DurationKey.AUTH).setError(error).commit();
|
|
2068
|
+
// Keep the superseded reference: it marks that a span existed in this era,
|
|
2069
|
+
// which blocks the auth-retry reset above.
|
|
2070
|
+
this.activeSpan?.cancel();
|
|
1921
2071
|
}
|
|
1922
2072
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2073
|
+
startSpan(channelUrl: string): ConversationInitialRenderStatsSpan {
|
|
2074
|
+
const superseded = this.activeSpan;
|
|
2075
|
+
superseded?.cancel();
|
|
2076
|
+
|
|
2077
|
+
const stats = this.getOrCreateStats();
|
|
2078
|
+
if (stats && superseded) stats.resetPageRenderSpan();
|
|
2079
|
+
|
|
2080
|
+
this.activeSpan = new ConversationInitialRenderStatsSpan(stats, channelUrl, this.visible);
|
|
2081
|
+
return this.activeSpan;
|
|
1926
2082
|
}
|
|
1927
2083
|
|
|
1928
2084
|
clear(): void {
|
|
2085
|
+
this.activeSpan?.cancel();
|
|
2086
|
+
this.activeSpan = null;
|
|
1929
2087
|
this.stats = null;
|
|
1930
|
-
this.
|
|
1931
|
-
this.channelUrl = undefined;
|
|
1932
|
-
this.consumed = false;
|
|
1933
|
-
this.consumedByAuthError = false;
|
|
1934
|
-
this.renderedAfterApiResult = false;
|
|
2088
|
+
this.authState = 'idle';
|
|
1935
2089
|
}
|
|
1936
2090
|
}
|
|
1937
2091
|
|
|
@@ -2065,153 +2219,263 @@ declare interface ConversationListHeaderTemplateProps {
|
|
|
2065
2219
|
titleAlign?: 'start' | 'center' | 'end';
|
|
2066
2220
|
}
|
|
2067
2221
|
|
|
2068
|
-
declare
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
private listStartedAt: number | null = null;
|
|
2075
|
-
private authStartedAt: number | null = null;
|
|
2076
|
-
private authDuration: number | null = null;
|
|
2077
|
-
private listDuration: number | null = null;
|
|
2222
|
+
declare type ConversationListInitialRenderMeasure = {
|
|
2223
|
+
listStarted: boolean;
|
|
2224
|
+
listCompleted: boolean;
|
|
2225
|
+
};
|
|
2226
|
+
|
|
2227
|
+
declare class ConversationListInitialRenderStats extends AIAgentBaseStats {
|
|
2078
2228
|
private presentMethod: PresentMethod = 'direct_present';
|
|
2079
|
-
private committed = false;
|
|
2080
2229
|
|
|
2081
|
-
|
|
2082
|
-
this.
|
|
2083
|
-
this
|
|
2230
|
+
setPresentMethod(method: PresentMethod): this {
|
|
2231
|
+
this.presentMethod = method;
|
|
2232
|
+
return this;
|
|
2084
2233
|
}
|
|
2085
2234
|
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
this
|
|
2092
|
-
this.startMeasurement();
|
|
2235
|
+
resetPageRenderSpan(): this {
|
|
2236
|
+
this.timers.delete(DurationKey.PAGE_RENDER);
|
|
2237
|
+
this.timers.delete(DurationKey.GET_LIST);
|
|
2238
|
+
this.errorCode = null;
|
|
2239
|
+
this.errorDescription = null;
|
|
2240
|
+
return this;
|
|
2093
2241
|
}
|
|
2094
2242
|
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
this.clearMeasurementState();
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
this.markListRequestStartOnce();
|
|
2243
|
+
protected getMetricKey(): string {
|
|
2244
|
+
return METRIC_KEY_CONVERSATION_LIST_INITIAL_RENDER;
|
|
2101
2245
|
}
|
|
2102
2246
|
|
|
2103
|
-
|
|
2104
|
-
this.
|
|
2105
|
-
|
|
2247
|
+
protected buildPayload(): AIAgentStatPayload | null {
|
|
2248
|
+
const auth = this.getTimer(DurationKey.AUTH);
|
|
2249
|
+
const pageRender = this.getTimer(DurationKey.PAGE_RENDER);
|
|
2250
|
+
const listDuration = this.getTimer(DurationKey.GET_LIST).duration;
|
|
2106
2251
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2252
|
+
// Only the auth-error early commit legitimately lacks a page render duration;
|
|
2253
|
+
// any other commit without it is an incomplete measurement and must not emit.
|
|
2254
|
+
if (pageRender.duration === null && (this.errorCode === null || auth.duration === null)) return null;
|
|
2110
2255
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2256
|
+
// The timers race freely; subtracting the overlap counts the auth wait exactly once.
|
|
2257
|
+
const pageRenderDuration = pageRender.duration === null ? 0 : pageRender.duration - overlapMs(auth, pageRender);
|
|
2258
|
+
const totalDuration = (auth.duration ?? 0) + pageRenderDuration;
|
|
2259
|
+
|
|
2260
|
+
return this.createPayload(totalDuration, {
|
|
2261
|
+
extra: {
|
|
2262
|
+
present_method: this.presentMethod,
|
|
2263
|
+
[DurationKey.AUTH]: auth.duration,
|
|
2264
|
+
[DurationKey.GET_LIST]: listDuration,
|
|
2265
|
+
},
|
|
2266
|
+
});
|
|
2113
2267
|
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
/**
|
|
2271
|
+
* One ConversationList entry attempt. Owns the list request phase; the tracker
|
|
2272
|
+
* owns the cross-attempt policy (auth span, supersession, one commit per entry).
|
|
2273
|
+
*
|
|
2274
|
+
* Timers record their own event boundaries and race freely against auth; the
|
|
2275
|
+
* metric subtracts the overlap at payload time, so event ordering needs no
|
|
2276
|
+
* orchestration here.
|
|
2277
|
+
*/
|
|
2278
|
+
declare class ConversationListInitialRenderStatsSpan extends StatsSpanBase<
|
|
2279
|
+
ConversationListInitialRenderStats,
|
|
2280
|
+
ConversationListInitialRenderMeasure
|
|
2281
|
+
> {
|
|
2282
|
+
protected measure: ConversationListInitialRenderMeasure = { listStarted: false, listCompleted: false };
|
|
2114
2283
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
this.
|
|
2118
|
-
if (presentMethod) this.presentMethod = presentMethod;
|
|
2284
|
+
constructor(stats: ConversationListInitialRenderStats | null, visible: boolean) {
|
|
2285
|
+
super(stats, visible);
|
|
2286
|
+
this.stats?.startTimer(DurationKey.PAGE_RENDER);
|
|
2119
2287
|
}
|
|
2120
2288
|
|
|
2121
|
-
|
|
2122
|
-
if (this.
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
this.
|
|
2127
|
-
this.
|
|
2289
|
+
onListRequestStart(): void {
|
|
2290
|
+
if (!this.stats || this.measure.listStarted) return;
|
|
2291
|
+
|
|
2292
|
+
// After a failed attempt cleared the timers via resetPageRenderSpan(), this
|
|
2293
|
+
// re-baselines them to the retry so the earlier failure and downtime are excluded.
|
|
2294
|
+
this.stats.startTimer(DurationKey.PAGE_RENDER);
|
|
2295
|
+
this.stats.startTimer(DurationKey.GET_LIST);
|
|
2296
|
+
this.measure.listStarted = true;
|
|
2128
2297
|
}
|
|
2129
2298
|
|
|
2130
|
-
|
|
2131
|
-
if (this.
|
|
2132
|
-
|
|
2133
|
-
|
|
2299
|
+
onListRequestComplete(error?: unknown): void {
|
|
2300
|
+
if (!this.stats || !this.measure.listStarted || this.measure.listCompleted) return;
|
|
2301
|
+
|
|
2302
|
+
if (error) {
|
|
2303
|
+
// Post-auth failures are not collected; drop the in-flight attempt so a retry restarts clean.
|
|
2304
|
+
this.stats.resetPageRenderSpan();
|
|
2305
|
+
this.measure.listStarted = false;
|
|
2134
2306
|
return;
|
|
2135
2307
|
}
|
|
2136
|
-
|
|
2137
|
-
this.
|
|
2308
|
+
|
|
2309
|
+
this.stats.stopTimer(DurationKey.GET_LIST);
|
|
2310
|
+
this.measure.listCompleted = true;
|
|
2138
2311
|
}
|
|
2139
2312
|
|
|
2140
|
-
|
|
2141
|
-
if (this.
|
|
2142
|
-
|
|
2143
|
-
this.
|
|
2313
|
+
onRendered(): void {
|
|
2314
|
+
if (!this.stats || !this.measure.listCompleted) return;
|
|
2315
|
+
|
|
2316
|
+
this.stats.stopTimer(DurationKey.PAGE_RENDER);
|
|
2317
|
+
this.tryFinalize();
|
|
2144
2318
|
}
|
|
2145
2319
|
|
|
2146
|
-
|
|
2147
|
-
if (this.
|
|
2148
|
-
|
|
2320
|
+
onRenderDiscarded(): void {
|
|
2321
|
+
if (!this.stats || !this.measure.listCompleted) return;
|
|
2322
|
+
|
|
2323
|
+
this.stats.discard();
|
|
2324
|
+
this.stats = null;
|
|
2149
2325
|
}
|
|
2150
2326
|
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
this.
|
|
2154
|
-
append(this.callback, METRIC_KEY_CONVERSATION_LIST_INITIAL_RENDER, this.clock.now() - this.startedAt, {
|
|
2155
|
-
extra: {
|
|
2156
|
-
...(this.authDuration !== null && { auth_duration_ms: this.authDuration }),
|
|
2157
|
-
...(this.listDuration !== null && { get_list_duration_ms: this.listDuration }),
|
|
2158
|
-
present_method: this.presentMethod,
|
|
2159
|
-
},
|
|
2160
|
-
});
|
|
2327
|
+
/** Auth settled while this span was measuring; a finalize that waited on in-flight auth can proceed. */
|
|
2328
|
+
onAuthSettled(): void {
|
|
2329
|
+
this.tryFinalize();
|
|
2161
2330
|
}
|
|
2162
2331
|
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
this.
|
|
2166
|
-
this.
|
|
2332
|
+
/** Commit once both facts exist: the render happened and auth is not in flight. */
|
|
2333
|
+
private tryFinalize(): void {
|
|
2334
|
+
if (!this.stats) return;
|
|
2335
|
+
if (this.stats.getTimer(DurationKey.PAGE_RENDER).endedAt === null) return;
|
|
2336
|
+
|
|
2337
|
+
const auth = this.stats.getTimer(DurationKey.AUTH);
|
|
2338
|
+
if (auth.startAt !== null && auth.endedAt === null) return;
|
|
2339
|
+
|
|
2340
|
+
this.commitOrDefer();
|
|
2167
2341
|
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
/**
|
|
2345
|
+
* Spec: conversation_list_initial_render
|
|
2346
|
+
*
|
|
2347
|
+
* Measures the first ConversationList screen render as the sum of auth and the
|
|
2348
|
+
* page render span required before the first rendered list frame.
|
|
2349
|
+
*
|
|
2350
|
+
* Timeline:
|
|
2351
|
+
* - Track auth independently; it can start before the list screen exists.
|
|
2352
|
+
* - Start a span when entering the display lifecycle.
|
|
2353
|
+
* - Track the list request phase on the span.
|
|
2354
|
+
* - Commit on the first rendered list frame. While the screen is hidden the
|
|
2355
|
+
* measurement still freezes at that frame, but emission is deferred until the
|
|
2356
|
+
* screen becomes visible.
|
|
2357
|
+
* - Discard (no payload) when the rendered frame was deferred by a
|
|
2358
|
+
* backgrounded page/app; the session stays consumed.
|
|
2359
|
+
*
|
|
2360
|
+
* Method call order:
|
|
2361
|
+
* - Resolve present method - setPresentMethod()
|
|
2362
|
+
* - Start auth - onAuthStart()
|
|
2363
|
+
* - Complete auth - onAuthComplete()
|
|
2364
|
+
* - Fail auth - onAuthError()
|
|
2365
|
+
* - Enter display lifecycle - startSpan()
|
|
2366
|
+
* - Screen visibility change - setVisible(visible)
|
|
2367
|
+
* - Start list request - span.onListRequestStart()
|
|
2368
|
+
* - Complete list request - span.onListRequestComplete()
|
|
2369
|
+
* - Fail list request - span.onListRequestComplete(error)
|
|
2370
|
+
* - Render list - span.onRendered()
|
|
2371
|
+
* - Render deferred by background - span.onRenderDiscarded()
|
|
2372
|
+
* - Abandon entry attempt - span.cancel()
|
|
2373
|
+
* - Logout (deauthenticate/dispose) - clear()
|
|
2374
|
+
*
|
|
2375
|
+
* Payload:
|
|
2376
|
+
* - value: total_duration_ms.
|
|
2377
|
+
* - extra.auth_duration_ms, get_list_duration_ms, present_method.
|
|
2378
|
+
*
|
|
2379
|
+
* Policy:
|
|
2380
|
+
* - Session-scoped; collect at most once per authenticated session. Only the
|
|
2381
|
+
* logout-time clear() reopens measurement: re-entering the list after a
|
|
2382
|
+
* commit is a warm navigation and is deliberately not re-measured.
|
|
2383
|
+
* - value = auth + (page render − overlap): both timers record their own event
|
|
2384
|
+
* boundaries and race freely, and the metric subtracts the overlapping
|
|
2385
|
+
* interval at payload time, so the auth wait is counted exactly once in any
|
|
2386
|
+
* ordering. The commit waits for in-flight auth to settle, since the value
|
|
2387
|
+
* needs the auth duration. A failed attempt re-baselines the page render
|
|
2388
|
+
* timer at its retry, excluding the failure and downtime.
|
|
2389
|
+
* - startSpan supersedes the previous span: the old span becomes non-recording
|
|
2390
|
+
* and the page render phase restarts. Auth data carries over to the new span.
|
|
2391
|
+
* - Success or auth failure closes collection until clear(). Post-auth failures
|
|
2392
|
+
* are not collected.
|
|
2393
|
+
* - An auth failure before any span is recording drops the pending measurement;
|
|
2394
|
+
* an auth retry starts a new one.
|
|
2395
|
+
* - A backgrounded page/app pauses requestAnimationFrame, so a render signal
|
|
2396
|
+
* arriving after background time would fold the idle dwell into
|
|
2397
|
+
* total_duration_ms. Such samples are discarded without a payload
|
|
2398
|
+
* (web-vitals convention) and consume the session like a commit.
|
|
2399
|
+
* - Measurement is independent of screen visibility: a hidden (launcher-closed
|
|
2400
|
+
* or route-inactive) preload keeps measuring with the same render criterion,
|
|
2401
|
+
* so visibility toggles never tear a measurement. Visibility only gates
|
|
2402
|
+
* emission: a render completed while hidden commits the moment the screen
|
|
2403
|
+
* becomes visible (setVisible), and a preload that is never shown is
|
|
2404
|
+
* cancelled with its span and never emitted.
|
|
2405
|
+
*/
|
|
2406
|
+
declare class ConversationListInitialRenderStatsTracker extends SpanStatsTrackerBase<ConversationListInitialRenderStatsSpan> {
|
|
2407
|
+
private stats: ConversationListInitialRenderStats | null = null;
|
|
2408
|
+
private authState: AuthState_2 = 'idle';
|
|
2409
|
+
private presentMethod: PresentMethod = 'direct_present';
|
|
2168
2410
|
|
|
2169
|
-
private
|
|
2170
|
-
this.
|
|
2171
|
-
this.
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2411
|
+
private getOrCreateStats(): ConversationListInitialRenderStats | null {
|
|
2412
|
+
if (this.stats?.isCommitted()) return null;
|
|
2413
|
+
if (!this.stats) {
|
|
2414
|
+
this.stats = new ConversationListInitialRenderStats()
|
|
2415
|
+
.setCommitCallback(this.commitCallback)
|
|
2416
|
+
.setPresentMethod(this.presentMethod);
|
|
2417
|
+
}
|
|
2418
|
+
return this.stats;
|
|
2175
2419
|
}
|
|
2176
2420
|
|
|
2177
|
-
private
|
|
2178
|
-
this.
|
|
2179
|
-
this.
|
|
2421
|
+
private getActiveStats(): ConversationListInitialRenderStats | null {
|
|
2422
|
+
if (!this.stats || this.stats.isCommitted()) return null;
|
|
2423
|
+
return this.stats;
|
|
2180
2424
|
}
|
|
2181
2425
|
|
|
2182
|
-
|
|
2183
|
-
|
|
2426
|
+
setPresentMethod(method: PresentMethod): void {
|
|
2427
|
+
this.presentMethod = method;
|
|
2428
|
+
this.getActiveStats()?.setPresentMethod(method);
|
|
2429
|
+
}
|
|
2184
2430
|
|
|
2185
|
-
|
|
2186
|
-
this.
|
|
2187
|
-
this.
|
|
2431
|
+
onAuthStart(): void {
|
|
2432
|
+
this.authState = 'in_progress';
|
|
2433
|
+
this.getOrCreateStats()?.startTimer(DurationKey.AUTH);
|
|
2188
2434
|
}
|
|
2189
2435
|
|
|
2190
|
-
|
|
2191
|
-
this.
|
|
2192
|
-
|
|
2436
|
+
onAuthComplete(): void {
|
|
2437
|
+
const wasInProgress = this.authState === 'in_progress';
|
|
2438
|
+
this.authState = 'completed';
|
|
2439
|
+
if (wasInProgress) this.getActiveStats()?.stopTimer(DurationKey.AUTH);
|
|
2193
2440
|
|
|
2194
|
-
this.
|
|
2195
|
-
this.measurement.listStarted = true;
|
|
2441
|
+
this.activeSpan?.onAuthSettled();
|
|
2196
2442
|
}
|
|
2197
2443
|
|
|
2198
|
-
|
|
2199
|
-
this.
|
|
2200
|
-
|
|
2444
|
+
onAuthError(error: Error): void {
|
|
2445
|
+
const wasInProgress = this.authState === 'in_progress';
|
|
2446
|
+
this.authState = 'failed';
|
|
2447
|
+
|
|
2448
|
+
const stats = this.getActiveStats();
|
|
2449
|
+
if (!stats) return;
|
|
2450
|
+
if (!this.activeSpan?.isRecording) {
|
|
2451
|
+
// Auth failed before any span was recording: drop the pending measurement so an auth retry re-measures cleanly.
|
|
2452
|
+
this.stats = null;
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
if (wasInProgress) stats.stopTimer(DurationKey.AUTH);
|
|
2201
2457
|
|
|
2202
|
-
|
|
2203
|
-
this.
|
|
2458
|
+
stats.setError(error).commit();
|
|
2459
|
+
this.activeSpan.cancel();
|
|
2204
2460
|
}
|
|
2205
2461
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2462
|
+
startSpan(): ConversationListInitialRenderStatsSpan {
|
|
2463
|
+
const superseded = this.activeSpan;
|
|
2464
|
+
superseded?.cancel();
|
|
2465
|
+
|
|
2466
|
+
const stats = this.getOrCreateStats();
|
|
2467
|
+
if (stats && superseded) stats.resetPageRenderSpan();
|
|
2468
|
+
if (stats && this.authState === 'in_progress') stats.startTimer(DurationKey.AUTH);
|
|
2208
2469
|
|
|
2209
|
-
this.
|
|
2210
|
-
|
|
2470
|
+
this.activeSpan = new ConversationListInitialRenderStatsSpan(stats, this.visible);
|
|
2471
|
+
return this.activeSpan;
|
|
2472
|
+
}
|
|
2211
2473
|
|
|
2212
|
-
|
|
2213
|
-
this.
|
|
2214
|
-
this.
|
|
2474
|
+
clear(): void {
|
|
2475
|
+
this.activeSpan?.cancel();
|
|
2476
|
+
this.activeSpan = null;
|
|
2477
|
+
this.stats = null;
|
|
2478
|
+
this.authState = 'idle';
|
|
2215
2479
|
}
|
|
2216
2480
|
}
|
|
2217
2481
|
|
|
@@ -2284,107 +2548,133 @@ export declare type ConversationListProps = PropsWithChildren<{
|
|
|
2284
2548
|
conversationListFilter?: Partial<AIAgentGroupChannelFilter>;
|
|
2285
2549
|
onOpenConversationView?: (channelUrl: string, status: 'open' | 'closed', options?: ConversationListOpenOptions) => void;
|
|
2286
2550
|
announcementsEnabled?: boolean;
|
|
2551
|
+
/** @default true */
|
|
2287
2552
|
conversationListStatsEnabled?: boolean;
|
|
2288
2553
|
/** Custom styles for the conversation list container. */
|
|
2289
2554
|
style?: CSSProperties;
|
|
2290
2555
|
}>;
|
|
2291
2556
|
|
|
2292
|
-
declare type
|
|
2293
|
-
|
|
2294
|
-
declare type ConversationMessageEventPayload =
|
|
2295
|
-
| {
|
|
2296
|
-
kind: 'messagesAdded';
|
|
2297
|
-
source: ChatSDKCollectionEventSource;
|
|
2298
|
-
messages: ChatSDKBaseMessage[];
|
|
2299
|
-
}
|
|
2300
|
-
| {
|
|
2301
|
-
kind: 'messagesUpdated';
|
|
2302
|
-
source: ChatSDKCollectionEventSource;
|
|
2303
|
-
messages: ChatSDKBaseMessage[];
|
|
2304
|
-
}
|
|
2305
|
-
| {
|
|
2306
|
-
kind: 'initialized';
|
|
2307
|
-
source: 'cache' | 'api';
|
|
2308
|
-
messages: ChatSDKBaseMessage[];
|
|
2309
|
-
};
|
|
2310
|
-
|
|
2311
|
-
declare type ConversationNotInteractiveMeasurement = {
|
|
2312
|
-
timer: TimeoutId | null;
|
|
2313
|
-
startedAt: number;
|
|
2314
|
-
committed: boolean;
|
|
2315
|
-
inputEnabled: boolean;
|
|
2316
|
-
phaseReached: ConversationNotInteractivePhase;
|
|
2317
|
-
historyLoadResult: HistoryLoadResult;
|
|
2318
|
-
conversationId?: number;
|
|
2557
|
+
declare type ConversationNotInteractiveMeasure = {
|
|
2319
2558
|
channelUrl?: string;
|
|
2559
|
+
historyLoadResult: HistoryLoadResult;
|
|
2560
|
+
inputEnabled: boolean;
|
|
2561
|
+
timer: TimeoutId | null;
|
|
2320
2562
|
};
|
|
2321
2563
|
|
|
2322
2564
|
declare type ConversationNotInteractivePhase = 'auth' | 'channel_join' | 'history_load' | 'input_enable' | 'render';
|
|
2323
2565
|
|
|
2324
|
-
declare class
|
|
2325
|
-
private
|
|
2326
|
-
private
|
|
2327
|
-
private readonly setTimeoutFn: typeof setTimeout;
|
|
2328
|
-
private readonly clearTimeoutFn: typeof clearTimeout;
|
|
2329
|
-
private readonly measurements = new Map<string, ConversationNotInteractiveMeasurement>();
|
|
2566
|
+
declare class ConversationNotInteractiveTimeoutStats extends AIAgentBaseStats {
|
|
2567
|
+
private phaseReached: ConversationNotInteractivePhase = 'auth';
|
|
2568
|
+
private historyLoadResult: HistoryLoadResult = 'pending';
|
|
2330
2569
|
|
|
2331
|
-
|
|
2332
|
-
this.
|
|
2333
|
-
this
|
|
2334
|
-
this.setTimeoutFn = getSetTimeout(options);
|
|
2335
|
-
this.clearTimeoutFn = getClearTimeout(options);
|
|
2570
|
+
setHistoryLoadResult(result: HistoryLoadResult): this {
|
|
2571
|
+
this.historyLoadResult = result;
|
|
2572
|
+
return this;
|
|
2336
2573
|
}
|
|
2337
2574
|
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
this
|
|
2341
|
-
|
|
2342
|
-
const measurement = {
|
|
2343
|
-
startedAt: this.clock.now(),
|
|
2344
|
-
channelUrl: params?.channelUrl,
|
|
2345
|
-
conversationId: params?.conversationId,
|
|
2346
|
-
committed: false,
|
|
2347
|
-
inputEnabled: false,
|
|
2348
|
-
phaseReached: 'auth' as ConversationNotInteractivePhase,
|
|
2349
|
-
historyLoadResult: 'pending' as HistoryLoadResult,
|
|
2350
|
-
timer: null as TimeoutId | null,
|
|
2351
|
-
};
|
|
2352
|
-
measurement.timer = this.setTimeoutFn(() => this.commitTimeout(key), 10_000);
|
|
2353
|
-
this.measurements.set(key, measurement);
|
|
2575
|
+
setPhaseReached(phase: ConversationNotInteractivePhase): this {
|
|
2576
|
+
this.phaseReached = phase;
|
|
2577
|
+
return this;
|
|
2354
2578
|
}
|
|
2355
2579
|
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
if (!measurement || measurement.committed) return;
|
|
2359
|
-
measurement.phaseReached = 'channel_join';
|
|
2360
|
-
if (conversationId !== undefined) measurement.conversationId = conversationId;
|
|
2580
|
+
protected getMetricKey(): string {
|
|
2581
|
+
return METRIC_KEY_CONVERSATION_NOT_INTERACTIVE_TIMEOUT;
|
|
2361
2582
|
}
|
|
2362
2583
|
|
|
2363
|
-
|
|
2364
|
-
const
|
|
2365
|
-
if (
|
|
2366
|
-
measurement.phaseReached = 'history_load';
|
|
2367
|
-
measurement.historyLoadResult = result;
|
|
2368
|
-
this.cancelIfReady(channelUrl);
|
|
2369
|
-
}
|
|
2584
|
+
protected buildPayload(): AIAgentStatPayload | null {
|
|
2585
|
+
const duration = this.getTimer(DurationKey.TIMEOUT).duration;
|
|
2586
|
+
if (duration === null) return null;
|
|
2370
2587
|
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2588
|
+
return this.createPayload(duration, {
|
|
2589
|
+
extra: {
|
|
2590
|
+
outcome: 'timeout',
|
|
2591
|
+
phase_reached: this.phaseReached,
|
|
2592
|
+
history_load_result: this.historyLoadResult,
|
|
2593
|
+
},
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
/**
|
|
2599
|
+
* Spec: conversation_not_interactive_timeout
|
|
2600
|
+
*
|
|
2601
|
+
* Detects a new Conversation entry that remains unusable for 10s.
|
|
2602
|
+
*
|
|
2603
|
+
* Timeline:
|
|
2604
|
+
* - Start when the Conversation screen display lifecycle begins.
|
|
2605
|
+
* - Track the farthest reached phase in auth -> channel_join -> history_load
|
|
2606
|
+
* -> input_enable -> render order.
|
|
2607
|
+
* - Success is input available and history display decision completed.
|
|
2608
|
+
* - Commit only if the 10s timeout fires before success.
|
|
2609
|
+
*
|
|
2610
|
+
* Payload:
|
|
2611
|
+
* - value: total_duration_ms.
|
|
2612
|
+
* - extra.phase_reached: farthest reached phase.
|
|
2613
|
+
* - extra.history_load_result: success | verified_empty | failed | pending.
|
|
2614
|
+
*
|
|
2615
|
+
* Policy:
|
|
2616
|
+
* - Entry-scoped; success or lifecycle abort clears the pending measurement.
|
|
2617
|
+
* - Placeholder/empty UI alone is not success unless the empty history result
|
|
2618
|
+
* is explicitly verified.
|
|
2619
|
+
*/
|
|
2620
|
+
declare class ConversationNotInteractiveTimeoutStatsTracker extends MultiStatsTrackerBase<
|
|
2621
|
+
ConversationNotInteractiveTimeoutStats,
|
|
2622
|
+
ConversationNotInteractiveMeasure
|
|
2623
|
+
> {
|
|
2624
|
+
start(params?: { channelUrl?: string; conversationId?: number }): void {
|
|
2625
|
+
const key = this.getKey(params?.channelUrl);
|
|
2626
|
+
this.clearKey(key);
|
|
2627
|
+
|
|
2628
|
+
const record: StatsRecord<ConversationNotInteractiveTimeoutStats, ConversationNotInteractiveMeasure> = {
|
|
2629
|
+
stats: new ConversationNotInteractiveTimeoutStats()
|
|
2630
|
+
.setCommitCallback(this.commitCallback)
|
|
2631
|
+
.startTimer(DurationKey.TIMEOUT),
|
|
2632
|
+
measure: {
|
|
2633
|
+
channelUrl: params?.channelUrl,
|
|
2634
|
+
historyLoadResult: 'pending',
|
|
2635
|
+
inputEnabled: false,
|
|
2636
|
+
timer: null,
|
|
2637
|
+
},
|
|
2638
|
+
};
|
|
2639
|
+
if (params?.conversationId !== undefined) record.stats.setConversationId(params.conversationId);
|
|
2640
|
+
record.measure.timer = setTimeout(() => this.commitTimeout(key), 10_000);
|
|
2641
|
+
this.records.set(key, record);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
markChannelJoined(conversationId?: number, channelUrl?: string): void {
|
|
2645
|
+
const record = this.getRecord(channelUrl);
|
|
2646
|
+
if (!record) return;
|
|
2647
|
+
record.stats.setPhaseReached('channel_join');
|
|
2648
|
+
if (conversationId !== undefined) record.stats.setConversationId(conversationId);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
markHistoryLoaded(result: HistoryLoadResult, channelUrl?: string): void {
|
|
2652
|
+
const record = this.getRecord(channelUrl);
|
|
2653
|
+
if (!record) return;
|
|
2654
|
+
record.measure.historyLoadResult = result;
|
|
2655
|
+
record.stats.setHistoryLoadResult(result).setPhaseReached('history_load');
|
|
2656
|
+
this.cancelIfReady(channelUrl);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
markInputReady(channelUrl?: string): void {
|
|
2660
|
+
const record = this.getRecord(channelUrl);
|
|
2661
|
+
if (!record) return;
|
|
2662
|
+
record.measure.inputEnabled = true;
|
|
2663
|
+
record.stats.setPhaseReached('input_enable');
|
|
2376
2664
|
this.cancelIfReady(channelUrl);
|
|
2377
2665
|
}
|
|
2378
2666
|
|
|
2379
2667
|
markRendered(channelUrl?: string): void {
|
|
2380
|
-
const
|
|
2381
|
-
if (!
|
|
2382
|
-
|
|
2668
|
+
const record = this.getRecord(channelUrl);
|
|
2669
|
+
if (!record) return;
|
|
2670
|
+
record.stats.setPhaseReached('render');
|
|
2383
2671
|
}
|
|
2384
2672
|
|
|
2385
2673
|
clear(): void {
|
|
2386
|
-
this.
|
|
2387
|
-
|
|
2674
|
+
this.records.forEach((record) => {
|
|
2675
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
2676
|
+
});
|
|
2677
|
+
this.records.clear();
|
|
2388
2678
|
}
|
|
2389
2679
|
|
|
2390
2680
|
clearChannel(channelUrl?: string): void {
|
|
@@ -2393,39 +2683,32 @@ declare class ConversationNotInteractiveTimeoutStatsTracker {
|
|
|
2393
2683
|
}
|
|
2394
2684
|
|
|
2395
2685
|
private clearKey(key: string): void {
|
|
2396
|
-
const
|
|
2397
|
-
if (!
|
|
2398
|
-
|
|
2399
|
-
this.
|
|
2686
|
+
const record = this.records.get(key);
|
|
2687
|
+
if (!record) return;
|
|
2688
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
2689
|
+
this.records.delete(key);
|
|
2400
2690
|
}
|
|
2401
2691
|
|
|
2402
2692
|
private cancelIfReady(channelUrl?: string): void {
|
|
2403
|
-
const
|
|
2404
|
-
if (!
|
|
2405
|
-
this.clearKey(this.getKey(
|
|
2693
|
+
const record = this.getRecord(channelUrl);
|
|
2694
|
+
if (!record || !record.measure.inputEnabled || record.measure.historyLoadResult === 'pending') return;
|
|
2695
|
+
this.clearKey(this.getKey(record.measure.channelUrl));
|
|
2406
2696
|
}
|
|
2407
2697
|
|
|
2408
2698
|
private commitTimeout(key: string): void {
|
|
2409
|
-
const
|
|
2410
|
-
if (!
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
conversationId: measurement.conversationId,
|
|
2415
|
-
error: { message: METRIC_KEY_CONVERSATION_NOT_INTERACTIVE_TIMEOUT },
|
|
2416
|
-
extra: {
|
|
2417
|
-
outcome: 'timeout',
|
|
2418
|
-
phase_reached: measurement.phaseReached,
|
|
2419
|
-
history_load_result: measurement.historyLoadResult,
|
|
2420
|
-
},
|
|
2421
|
-
});
|
|
2422
|
-
this.measurements.delete(key);
|
|
2699
|
+
const record = this.records.get(key);
|
|
2700
|
+
if (!record) return;
|
|
2701
|
+
record.measure.timer = null;
|
|
2702
|
+
this.records.delete(key);
|
|
2703
|
+
record.stats.stopTimer(DurationKey.TIMEOUT).commit();
|
|
2423
2704
|
}
|
|
2424
2705
|
|
|
2425
|
-
private
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
return this.
|
|
2706
|
+
private getRecord(
|
|
2707
|
+
channelUrl?: string,
|
|
2708
|
+
): StatsRecord<ConversationNotInteractiveTimeoutStats, ConversationNotInteractiveMeasure> | undefined {
|
|
2709
|
+
if (channelUrl !== undefined) return this.records.get(this.getKey(channelUrl));
|
|
2710
|
+
if (this.records.size === 1) return this.records.values().next().value;
|
|
2711
|
+
return this.records.get(this.getKey());
|
|
2429
2712
|
}
|
|
2430
2713
|
|
|
2431
2714
|
private getKey(channelUrl?: string): string {
|
|
@@ -2440,7 +2723,14 @@ export declare type ConversationProps = PropsWithChildren<{
|
|
|
2440
2723
|
shouldMarkAsRead?: boolean;
|
|
2441
2724
|
/** @internal */
|
|
2442
2725
|
shouldInitializeConversation?: boolean;
|
|
2726
|
+
/**
|
|
2727
|
+
* Message to send as the first user message when the conversation is initialized
|
|
2728
|
+
* on the server (e.g., the callback user message of a clicked nudge message).
|
|
2729
|
+
* Applied only when the channel has no conversation yet.
|
|
2730
|
+
*/
|
|
2731
|
+
initialUserMessage?: string;
|
|
2443
2732
|
announcementsEnabled?: boolean;
|
|
2733
|
+
/** @default true */
|
|
2444
2734
|
conversationStatsEnabled?: boolean;
|
|
2445
2735
|
initialFocusTarget?: ConversationInitialFocusTarget;
|
|
2446
2736
|
/** Custom styles for the conversation container. */
|
|
@@ -2638,11 +2928,20 @@ declare interface Dispatcher {
|
|
|
2638
2928
|
}
|
|
2639
2929
|
|
|
2640
2930
|
declare enum DurationKey {
|
|
2931
|
+
ACK_TO_FIRST_CHUNK = 'ack_to_first_chunk_ms',
|
|
2932
|
+
AFTER_TPEN_ABSENT = 'after_tpen_absent_ms',
|
|
2641
2933
|
AUTH = 'auth_duration_ms',
|
|
2934
|
+
BEFORE_TPST_ABSENT = 'before_tpst_absent_ms',
|
|
2935
|
+
FIRST_CHUNK_TO_RENDER = 'first_chunk_to_render_ms',
|
|
2642
2936
|
GET_CHANNEL = 'get_channel_duration_ms',
|
|
2937
|
+
GET_LIST = 'get_list_duration_ms',
|
|
2643
2938
|
GET_MESSAGES = 'get_messages_duration_ms',
|
|
2644
|
-
|
|
2645
|
-
|
|
2939
|
+
// Internal timer key; the emitted value derives from it and is never emitted raw.
|
|
2940
|
+
PAGE_RENDER = 'page_render_duration_ms',
|
|
2941
|
+
// Emitted field name predates the auth/page-render split; kept for dashboard compatibility.
|
|
2942
|
+
PAGE_RENDER_FROM_CACHE = 'total_duration_from_cache_ms',
|
|
2943
|
+
SEND_TO_ACK = 'send_to_ack_ms',
|
|
2944
|
+
TIMEOUT = 'timeout_ms',
|
|
2646
2945
|
}
|
|
2647
2946
|
|
|
2648
2947
|
declare type ErrorAnnouncementParams =
|
|
@@ -2651,8 +2950,6 @@ declare type ErrorAnnouncementParams =
|
|
|
2651
2950
|
| { type: 'csatFormError' }
|
|
2652
2951
|
| { type: 'generic'; message: string };
|
|
2653
2952
|
|
|
2654
|
-
declare type ErrorLike = StatsPayloadError;
|
|
2655
|
-
|
|
2656
2953
|
/**
|
|
2657
2954
|
* For better understanding: https://sendbird.atlassian.net/wiki/spaces/AA/pages/3075014695/Extended+Message+Payload+Spec
|
|
2658
2955
|
*/
|
|
@@ -2821,16 +3118,6 @@ export declare interface FunctionCallsInfo {
|
|
|
2821
3118
|
};
|
|
2822
3119
|
}
|
|
2823
3120
|
|
|
2824
|
-
declare type GetChannelCompleteParams = {
|
|
2825
|
-
channelUrl?: string;
|
|
2826
|
-
conversationId?: number;
|
|
2827
|
-
};
|
|
2828
|
-
|
|
2829
|
-
declare type GetChannelErrorParams = {
|
|
2830
|
-
channelUrl?: string;
|
|
2831
|
-
error: Error;
|
|
2832
|
-
};
|
|
2833
|
-
|
|
2834
3121
|
export declare interface GroundednessInfo {
|
|
2835
3122
|
id: number;
|
|
2836
3123
|
source_type:
|
|
@@ -5210,92 +5497,139 @@ export declare const MessageListUILayout: {
|
|
|
5210
5497
|
|
|
5211
5498
|
export declare const MessageLogs: ({ actionbook, functionCalls, groundedness, agentMessageTemplates, flaggedTypes, onClickActionbook, onClickFunctionCall, onClickFunctionCallDetail, onClickGroundedness, onClickAgentMessageTemplate, topContent, bottomContent, renderCustomGroundednessIcon, style, }: Props_2) => JSX.Element;
|
|
5212
5499
|
|
|
5213
|
-
declare class
|
|
5214
|
-
private
|
|
5215
|
-
private
|
|
5216
|
-
private
|
|
5217
|
-
private readonly measurements = new Map<string, PendingMeasurement>();
|
|
5500
|
+
declare class MessagePendingStuckStats extends AIAgentBaseStats {
|
|
5501
|
+
private requestId?: string;
|
|
5502
|
+
private messageType?: PendingMessageType;
|
|
5503
|
+
private stuckReason?: PendingStuckReason;
|
|
5218
5504
|
|
|
5219
|
-
|
|
5220
|
-
this.
|
|
5221
|
-
this
|
|
5222
|
-
this.clearTimeoutFn = getClearTimeout(options);
|
|
5505
|
+
setRequestId(requestId: string): this {
|
|
5506
|
+
this.requestId = requestId;
|
|
5507
|
+
return this;
|
|
5223
5508
|
}
|
|
5224
5509
|
|
|
5510
|
+
setMessageType(messageType: PendingMessageType): this {
|
|
5511
|
+
this.messageType = messageType;
|
|
5512
|
+
return this;
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
setStuckReason(stuckReason: PendingStuckReason): this {
|
|
5516
|
+
this.stuckReason = stuckReason;
|
|
5517
|
+
return this;
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5520
|
+
protected getMetricKey(): string {
|
|
5521
|
+
return METRIC_KEY_MESSAGE_PENDING_STUCK;
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
protected buildPayload(): AIAgentStatPayload | null {
|
|
5525
|
+
if (this.requestId === undefined || this.messageType === undefined || this.stuckReason === undefined) return null;
|
|
5526
|
+
|
|
5527
|
+
return this.createPayload(this.stuckReason, {
|
|
5528
|
+
extra: {
|
|
5529
|
+
message_type: this.messageType,
|
|
5530
|
+
request_id: this.requestId,
|
|
5531
|
+
},
|
|
5532
|
+
});
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5536
|
+
/**
|
|
5537
|
+
* Spec: message_pending_stuck
|
|
5538
|
+
*
|
|
5539
|
+
* Detects a user message send that stays pending too long.
|
|
5540
|
+
*
|
|
5541
|
+
* Timeline:
|
|
5542
|
+
* - Start after the SDK registers a pending send with a request_id.
|
|
5543
|
+
* - Text messages commit after 5s without send completion.
|
|
5544
|
+
* - File messages commit after 10s without upload progress or send completion.
|
|
5545
|
+
* - Upload progress resets the file no-progress timer.
|
|
5546
|
+
*
|
|
5547
|
+
* Payload:
|
|
5548
|
+
* - value: stuck_reason = pending_too_long | upload_no_progress.
|
|
5549
|
+
* - extra.message_type, request_id.
|
|
5550
|
+
*
|
|
5551
|
+
* Policy:
|
|
5552
|
+
* - Send-scoped; each request_id is measured independently.
|
|
5553
|
+
* - Send completion, disappearance, or lifecycle abort clears the measurement
|
|
5554
|
+
* without emitting.
|
|
5555
|
+
* - Offline sends are excluded from measurement.
|
|
5556
|
+
* - There is no duration value and no resolved follow-up stat.
|
|
5557
|
+
*/
|
|
5558
|
+
declare class MessagePendingStuckStatsTracker extends MultiStatsTrackerBase<MessagePendingStuckStats, PendingMeasure> {
|
|
5225
5559
|
start(params: {
|
|
5226
5560
|
requestId: string;
|
|
5227
5561
|
channelUrl?: string;
|
|
5228
5562
|
messageType: PendingMessageType;
|
|
5229
5563
|
tracksUploadProgress?: boolean;
|
|
5230
5564
|
}): void {
|
|
5231
|
-
if (!params.requestId || this.
|
|
5565
|
+
if (!params.requestId || this.records.has(params.requestId)) return;
|
|
5232
5566
|
|
|
5233
5567
|
const usesUploadProgress = params.messageType !== 'text' && params.tracksUploadProgress === true;
|
|
5234
|
-
const
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5568
|
+
const stats = new MessagePendingStuckStats()
|
|
5569
|
+
.setRequestId(params.requestId)
|
|
5570
|
+
.setMessageType(params.messageType)
|
|
5571
|
+
.setStuckReason(usesUploadProgress ? 'upload_no_progress' : 'pending_too_long')
|
|
5572
|
+
.setCommitCallback(this.commitCallback);
|
|
5573
|
+
const record: StatsRecord<MessagePendingStuckStats, PendingMeasure> = {
|
|
5574
|
+
stats,
|
|
5575
|
+
measure: {
|
|
5576
|
+
channelUrl: params.channelUrl,
|
|
5577
|
+
timer: null,
|
|
5578
|
+
usesUploadProgress,
|
|
5579
|
+
},
|
|
5239
5580
|
};
|
|
5240
5581
|
const timeoutMs = usesUploadProgress ? FILE_UPLOAD_NO_PROGRESS_TIMEOUT_MS : TEXT_PENDING_TIMEOUT_MS;
|
|
5241
|
-
|
|
5242
|
-
this.
|
|
5582
|
+
record.measure.timer = setTimeout(() => this.detect(params.requestId), timeoutMs);
|
|
5583
|
+
this.records.set(params.requestId, record);
|
|
5243
5584
|
}
|
|
5244
5585
|
|
|
5245
5586
|
resolve(params: { requestId: string }): void {
|
|
5246
|
-
|
|
5247
|
-
if (!measurement) return;
|
|
5248
|
-
|
|
5249
|
-
clearTimer(measurement.timer, this.clearTimeoutFn);
|
|
5250
|
-
this.measurements.delete(params.requestId);
|
|
5587
|
+
this.cancel(params.requestId);
|
|
5251
5588
|
}
|
|
5252
5589
|
|
|
5253
5590
|
disappear(params: { requestId: string }): void {
|
|
5254
|
-
|
|
5255
|
-
|
|
5591
|
+
this.cancel(params.requestId);
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
private cancel(requestId: string): void {
|
|
5595
|
+
const record = this.records.get(requestId);
|
|
5596
|
+
if (!record) return;
|
|
5256
5597
|
|
|
5257
|
-
|
|
5258
|
-
this.
|
|
5598
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
5599
|
+
this.records.delete(requestId);
|
|
5259
5600
|
}
|
|
5260
5601
|
|
|
5261
5602
|
markUploadProgress(requestId: string): void {
|
|
5262
|
-
const
|
|
5263
|
-
if (!
|
|
5264
|
-
if (
|
|
5603
|
+
const record = this.records.get(requestId);
|
|
5604
|
+
if (!record) return;
|
|
5605
|
+
if (!record.measure.usesUploadProgress) return;
|
|
5265
5606
|
|
|
5266
|
-
|
|
5267
|
-
|
|
5607
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
5608
|
+
record.measure.timer = setTimeout(() => this.detect(requestId), FILE_UPLOAD_NO_PROGRESS_TIMEOUT_MS);
|
|
5268
5609
|
}
|
|
5269
5610
|
|
|
5270
5611
|
clear(): void {
|
|
5271
|
-
this.
|
|
5272
|
-
|
|
5612
|
+
this.records.forEach((record) => {
|
|
5613
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
5614
|
+
});
|
|
5615
|
+
this.records.clear();
|
|
5273
5616
|
}
|
|
5274
5617
|
|
|
5275
5618
|
clearChannel(channelUrl?: string): void {
|
|
5276
5619
|
if (!channelUrl) return;
|
|
5277
|
-
this.
|
|
5278
|
-
if (
|
|
5279
|
-
|
|
5280
|
-
this.
|
|
5620
|
+
this.records.forEach((record, requestId) => {
|
|
5621
|
+
if (record.measure.channelUrl !== channelUrl) return;
|
|
5622
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
5623
|
+
this.records.delete(requestId);
|
|
5281
5624
|
});
|
|
5282
5625
|
}
|
|
5283
5626
|
|
|
5284
5627
|
private detect(requestId: string): void {
|
|
5285
|
-
const
|
|
5286
|
-
if (!
|
|
5628
|
+
const record = this.records.get(requestId);
|
|
5629
|
+
if (!record) return;
|
|
5287
5630
|
|
|
5288
|
-
this.
|
|
5289
|
-
|
|
5290
|
-
}
|
|
5291
|
-
|
|
5292
|
-
private commit(requestId: string, measurement: PendingMeasurement): void {
|
|
5293
|
-
append(this.callback, METRIC_KEY_MESSAGE_PENDING_STUCK, measurement.stuckReason, {
|
|
5294
|
-
extra: {
|
|
5295
|
-
message_type: measurement.messageType,
|
|
5296
|
-
request_id: requestId,
|
|
5297
|
-
},
|
|
5298
|
-
});
|
|
5631
|
+
this.records.delete(requestId);
|
|
5632
|
+
record.stats.commit();
|
|
5299
5633
|
}
|
|
5300
5634
|
}
|
|
5301
5635
|
|
|
@@ -5515,6 +5849,10 @@ declare type MultiSelectField = {
|
|
|
5515
5849
|
};
|
|
5516
5850
|
};
|
|
5517
5851
|
|
|
5852
|
+
declare abstract class MultiStatsTrackerBase<TStats, TMeasure> extends StatsTrackerBase {
|
|
5853
|
+
protected readonly records = new Map<string | number, StatsRecord<TStats, TMeasure>>();
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5518
5856
|
declare interface NetworkStateAdapter {
|
|
5519
5857
|
/** Returns true if network is currently online */
|
|
5520
5858
|
isOnline: () => boolean;
|
|
@@ -5522,6 +5860,13 @@ declare interface NetworkStateAdapter {
|
|
|
5522
5860
|
subscribe: (callback: (isOnline: boolean) => void) => () => void;
|
|
5523
5861
|
}
|
|
5524
5862
|
|
|
5863
|
+
export declare interface NudgeMessageResponse {
|
|
5864
|
+
/** Message to display above the launcher. */
|
|
5865
|
+
message: string;
|
|
5866
|
+
/** Message to send as the first user message when the nudge is clicked. */
|
|
5867
|
+
callbackUserMessage?: string;
|
|
5868
|
+
}
|
|
5869
|
+
|
|
5525
5870
|
declare type NumberField = {
|
|
5526
5871
|
key: string;
|
|
5527
5872
|
type: 'number';
|
|
@@ -5665,6 +6010,14 @@ declare type OutgoingMessageUnion =
|
|
|
5665
6010
|
|
|
5666
6011
|
declare function OutgoingTextBody(props: OutgoingMessageBodyProps): JSX.Element;
|
|
5667
6012
|
|
|
6013
|
+
declare type PageRenderState =
|
|
6014
|
+
| 'initialized'
|
|
6015
|
+
| 'channel_fetching'
|
|
6016
|
+
| 'channel_fetched'
|
|
6017
|
+
| 'channel_failed'
|
|
6018
|
+
| 'messages_fetching'
|
|
6019
|
+
| 'messages_fetching_completed';
|
|
6020
|
+
|
|
5668
6021
|
declare interface PaletteColor {
|
|
5669
6022
|
extraDark: string;
|
|
5670
6023
|
dark: string;
|
|
@@ -5713,19 +6066,16 @@ declare type PendingAIResponseStart = {
|
|
|
5713
6066
|
startedAt: number;
|
|
5714
6067
|
};
|
|
5715
6068
|
|
|
5716
|
-
declare type
|
|
6069
|
+
declare type PendingMeasure = {
|
|
5717
6070
|
channelUrl?: string;
|
|
5718
|
-
messageType: PendingMessageType;
|
|
5719
|
-
stuckReason: PendingStuckReason;
|
|
5720
6071
|
timer: TimeoutId | null;
|
|
6072
|
+
usesUploadProgress: boolean;
|
|
5721
6073
|
};
|
|
5722
6074
|
|
|
5723
6075
|
declare type PendingMessageType = 'text' | 'file_image' | 'file_video' | 'file_document' | 'file_audio';
|
|
5724
6076
|
|
|
5725
6077
|
declare type PendingStuckReason = 'pending_too_long' | 'upload_no_progress';
|
|
5726
6078
|
|
|
5727
|
-
declare type PendingStuckTrackerOptions = Pick<TimedTrackerOptions, 'setTimeout' | 'clearTimeout'>;
|
|
5728
|
-
|
|
5729
6079
|
declare type PickMessageProps<
|
|
5730
6080
|
Union,
|
|
5731
6081
|
T extends Union extends { messageType: infer MT } ? MT : never = Union extends { messageType: infer MT } ? MT : never,
|
|
@@ -5863,74 +6213,113 @@ declare type Props_2 = {
|
|
|
5863
6213
|
|
|
5864
6214
|
declare type ReactOnlyIconName = 'expand' | 'collapse' | 'chevron-right' | 'attach' | 'close-filled' | 'actionbook' | 'function' | 'confluence' | 'zendesk' | 'salesforce' | 'sprinklr' | 'website' | 'snippet' | 'template' | 'show' | 'mute' | 'activity';
|
|
5865
6215
|
|
|
5866
|
-
declare type
|
|
5867
|
-
timer: TimeoutId | null;
|
|
5868
|
-
startedAt: number | null;
|
|
5869
|
-
triggerType: RecoveryTriggerType | null;
|
|
6216
|
+
declare type RecoveryMeasure = {
|
|
5870
6217
|
inputEnabled: boolean;
|
|
6218
|
+
started: boolean;
|
|
6219
|
+
timer: TimeoutId | null;
|
|
5871
6220
|
};
|
|
5872
6221
|
|
|
5873
|
-
declare class
|
|
5874
|
-
private
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
6222
|
+
declare class RecoveryNotInteractiveTimeoutStats extends AIAgentBaseStats {
|
|
6223
|
+
private triggerType?: RecoveryTriggerType;
|
|
6224
|
+
|
|
6225
|
+
setTriggerType(triggerType: RecoveryTriggerType): this {
|
|
6226
|
+
this.triggerType = triggerType;
|
|
6227
|
+
return this;
|
|
6228
|
+
}
|
|
6229
|
+
|
|
6230
|
+
protected getMetricKey(): string {
|
|
6231
|
+
return METRIC_KEY_RECOVERY_NOT_INTERACTIVE_TIMEOUT;
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
protected buildPayload(): AIAgentStatPayload | null {
|
|
6235
|
+
const duration = this.getTimer(DurationKey.TIMEOUT).duration;
|
|
6236
|
+
if (duration === null || this.triggerType === undefined) return null;
|
|
5879
6237
|
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
6238
|
+
return this.createPayload(duration, {
|
|
6239
|
+
extra: {
|
|
6240
|
+
trigger_type: this.triggerType,
|
|
6241
|
+
},
|
|
6242
|
+
});
|
|
5885
6243
|
}
|
|
6244
|
+
}
|
|
5886
6245
|
|
|
6246
|
+
/**
|
|
6247
|
+
* Spec: recovery_not_interactive_timeout
|
|
6248
|
+
*
|
|
6249
|
+
* Detects a Conversation that does not become interactive within 10s after a
|
|
6250
|
+
* recovery trigger.
|
|
6251
|
+
*
|
|
6252
|
+
* Timeline:
|
|
6253
|
+
* - Start after foreground_resume when the app was backgrounded for at least
|
|
6254
|
+
* 5s, or after ws_reconnected.
|
|
6255
|
+
* - Skip the measurement when the input is already enabled.
|
|
6256
|
+
* - Cancel when input becomes enabled before the timeout.
|
|
6257
|
+
* - Commit only if the 10s timeout fires while input is still unavailable.
|
|
6258
|
+
*
|
|
6259
|
+
* Payload:
|
|
6260
|
+
* - value: total_duration_ms.
|
|
6261
|
+
* - extra.trigger_type: foreground_resume | ws_reconnected.
|
|
6262
|
+
*
|
|
6263
|
+
* Policy:
|
|
6264
|
+
* - Recovery-cycle scoped; one active measurement per Conversation/recovery
|
|
6265
|
+
* trigger.
|
|
6266
|
+
* - Successful recovery is not recorded by this metric.
|
|
6267
|
+
*/
|
|
6268
|
+
declare class RecoveryNotInteractiveTimeoutStatsTracker extends MultiStatsTrackerBase<
|
|
6269
|
+
RecoveryNotInteractiveTimeoutStats,
|
|
6270
|
+
RecoveryMeasure
|
|
6271
|
+
> {
|
|
5887
6272
|
start(triggerType: RecoveryTriggerType, channelUrl?: string): void {
|
|
5888
6273
|
const key = this.getKey(channelUrl);
|
|
5889
|
-
let
|
|
5890
|
-
if (
|
|
5891
|
-
if (
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
6274
|
+
let record = this.records.get(key);
|
|
6275
|
+
if (record?.measure.started) return;
|
|
6276
|
+
if (record?.measure.inputEnabled) return;
|
|
6277
|
+
|
|
6278
|
+
if (!record) {
|
|
6279
|
+
record = this.createRecord(false);
|
|
6280
|
+
}
|
|
6281
|
+
|
|
6282
|
+
record.measure.started = true;
|
|
6283
|
+
record.stats = new RecoveryNotInteractiveTimeoutStats()
|
|
6284
|
+
.setTriggerType(triggerType)
|
|
6285
|
+
.setCommitCallback(this.commitCallback)
|
|
6286
|
+
.startTimer(DurationKey.TIMEOUT);
|
|
6287
|
+
record.measure.timer = setTimeout(() => this.commitTimeout(key), 10_000);
|
|
6288
|
+
this.records.set(key, record);
|
|
5900
6289
|
}
|
|
5901
6290
|
|
|
5902
6291
|
setInputEnabled(enabled: boolean, channelUrl?: string): void {
|
|
5903
6292
|
const key = this.getKey(channelUrl);
|
|
5904
|
-
let
|
|
6293
|
+
let record = this.records.get(key);
|
|
5905
6294
|
|
|
5906
|
-
if (!
|
|
6295
|
+
if (!record) {
|
|
5907
6296
|
if (!enabled) return;
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
inputEnabled: enabled,
|
|
5912
|
-
timer: null,
|
|
5913
|
-
};
|
|
5914
|
-
this.measurements.set(key, measurement);
|
|
6297
|
+
record = this.createRecord(enabled);
|
|
6298
|
+
this.records.set(key, record);
|
|
6299
|
+
return;
|
|
5915
6300
|
}
|
|
5916
6301
|
|
|
5917
|
-
|
|
6302
|
+
record.measure.inputEnabled = enabled;
|
|
5918
6303
|
if (enabled) {
|
|
5919
|
-
this.
|
|
6304
|
+
this.cancelRecord(record);
|
|
6305
|
+
} else if (!record.measure.started) {
|
|
6306
|
+
this.records.delete(key);
|
|
5920
6307
|
}
|
|
5921
6308
|
}
|
|
5922
6309
|
|
|
5923
6310
|
cancelPending(channelUrl: string): void {
|
|
5924
6311
|
const key = this.getKey(channelUrl);
|
|
5925
|
-
const
|
|
5926
|
-
if (!
|
|
5927
|
-
this.
|
|
5928
|
-
if (!
|
|
6312
|
+
const record = this.records.get(key);
|
|
6313
|
+
if (!record) return;
|
|
6314
|
+
this.cancelRecord(record);
|
|
6315
|
+
if (!record.measure.inputEnabled) this.records.delete(key);
|
|
5929
6316
|
}
|
|
5930
6317
|
|
|
5931
6318
|
clear(): void {
|
|
5932
|
-
this.
|
|
5933
|
-
|
|
6319
|
+
this.records.forEach((record) => {
|
|
6320
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
6321
|
+
});
|
|
6322
|
+
this.records.clear();
|
|
5934
6323
|
}
|
|
5935
6324
|
|
|
5936
6325
|
clearChannel(channelUrl?: string): void {
|
|
@@ -5939,35 +6328,41 @@ declare class RecoveryNotInteractiveTimeoutStatsTracker {
|
|
|
5939
6328
|
}
|
|
5940
6329
|
|
|
5941
6330
|
private clearKey(key: string): void {
|
|
5942
|
-
const
|
|
5943
|
-
if (!
|
|
5944
|
-
|
|
5945
|
-
this.
|
|
6331
|
+
const record = this.records.get(key);
|
|
6332
|
+
if (!record) return;
|
|
6333
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
6334
|
+
this.records.delete(key);
|
|
5946
6335
|
}
|
|
5947
6336
|
|
|
5948
6337
|
private commitTimeout(key: string): void {
|
|
5949
|
-
const
|
|
5950
|
-
if (!
|
|
5951
|
-
if (
|
|
5952
|
-
this.
|
|
6338
|
+
const record = this.records.get(key);
|
|
6339
|
+
if (!record || !record.measure.started) return;
|
|
6340
|
+
if (record.measure.inputEnabled) {
|
|
6341
|
+
this.cancelRecord(record);
|
|
5953
6342
|
return;
|
|
5954
6343
|
}
|
|
5955
|
-
|
|
5956
|
-
const triggerType = measurement.triggerType;
|
|
6344
|
+
record.stats.stopTimer(DurationKey.TIMEOUT);
|
|
5957
6345
|
// Drop the entry so a subsequent trigger can record a fresh cycle.
|
|
5958
|
-
this.
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
6346
|
+
this.records.delete(key);
|
|
6347
|
+
record.stats.commit();
|
|
6348
|
+
}
|
|
6349
|
+
|
|
6350
|
+
private cancelRecord(record: StatsRecord<RecoveryNotInteractiveTimeoutStats, RecoveryMeasure>): void {
|
|
6351
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
6352
|
+
record.measure.timer = null;
|
|
6353
|
+
record.measure.started = false;
|
|
6354
|
+
record.stats = new RecoveryNotInteractiveTimeoutStats().setCommitCallback(this.commitCallback);
|
|
5964
6355
|
}
|
|
5965
6356
|
|
|
5966
|
-
private
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
6357
|
+
private createRecord(inputEnabled: boolean): StatsRecord<RecoveryNotInteractiveTimeoutStats, RecoveryMeasure> {
|
|
6358
|
+
return {
|
|
6359
|
+
stats: new RecoveryNotInteractiveTimeoutStats().setCommitCallback(this.commitCallback),
|
|
6360
|
+
measure: {
|
|
6361
|
+
inputEnabled,
|
|
6362
|
+
started: false,
|
|
6363
|
+
timer: null,
|
|
6364
|
+
},
|
|
6365
|
+
};
|
|
5971
6366
|
}
|
|
5972
6367
|
|
|
5973
6368
|
private getKey(channelUrl?: string): string {
|
|
@@ -5987,39 +6382,79 @@ declare type ReleaseDisabledByValue =
|
|
|
5987
6382
|
| 'reconnecting'
|
|
5988
6383
|
| 'handoff_pending';
|
|
5989
6384
|
|
|
5990
|
-
declare
|
|
5991
|
-
|
|
5992
|
-
|
|
6385
|
+
declare type RenderedContentError = {
|
|
6386
|
+
code?: number;
|
|
6387
|
+
message?: string;
|
|
6388
|
+
};
|
|
5993
6389
|
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
6390
|
+
declare type RenderedContentErrorDomain = 'empty_render' | 'template_error' | 'form_error';
|
|
6391
|
+
|
|
6392
|
+
declare type RenderedContentKind = 'text' | 'image' | 'video' | 'audio' | 'template' | 'form';
|
|
6393
|
+
|
|
6394
|
+
declare type RenderedContentUnusableMeasure = {
|
|
6395
|
+
committed: Set<string>;
|
|
6396
|
+
};
|
|
6397
|
+
|
|
6398
|
+
/**
|
|
6399
|
+
* Spec: rendered_content_unusable
|
|
6400
|
+
*
|
|
6401
|
+
* Records AI response content that reaches the render pipeline but cannot be
|
|
6402
|
+
* presented as usable visible content.
|
|
6403
|
+
*
|
|
6404
|
+
* Timeline:
|
|
6405
|
+
* - Trigger on final render pipeline failures such as empty_render or
|
|
6406
|
+
* template_error.
|
|
6407
|
+
* - Commit immediately when the SDK-owned renderer determines the content is
|
|
6408
|
+
* unusable.
|
|
6409
|
+
*
|
|
6410
|
+
* Payload:
|
|
6411
|
+
* - value: error_domain.
|
|
6412
|
+
* - extra.content_kind, message_id.
|
|
6413
|
+
*
|
|
6414
|
+
* Policy:
|
|
6415
|
+
* - Message/content scoped; duplicate failures for the same message, content
|
|
6416
|
+
* kind, and domain are suppressed.
|
|
6417
|
+
* - Transient network failures and retries that later render successfully are
|
|
6418
|
+
* not the target of this metric.
|
|
6419
|
+
*/
|
|
6420
|
+
declare class RenderedContentUnusableStatsTracker extends StatsTrackerBase {
|
|
6421
|
+
private readonly measure: RenderedContentUnusableMeasure = {
|
|
6422
|
+
committed: new Set<string>(),
|
|
6423
|
+
};
|
|
5997
6424
|
|
|
5998
6425
|
commit(params: {
|
|
5999
6426
|
messageId: string | number;
|
|
6000
|
-
contentKind:
|
|
6001
|
-
errorDomain:
|
|
6002
|
-
error?:
|
|
6427
|
+
contentKind: RenderedContentKind;
|
|
6428
|
+
errorDomain: RenderedContentErrorDomain;
|
|
6429
|
+
error?: RenderedContentError;
|
|
6003
6430
|
}): void {
|
|
6004
6431
|
const key = `${params.messageId}:${params.contentKind}:${params.errorDomain}`;
|
|
6005
|
-
if (this.committed.has(key)) return;
|
|
6006
|
-
evictOldestIfAtLimit(this.committed, RENDERED_CONTENT_UNUSABLE_COMMITTED_LIMIT);
|
|
6007
|
-
this.committed.add(key);
|
|
6432
|
+
if (this.measure.committed.has(key)) return;
|
|
6433
|
+
evictOldestIfAtLimit(this.measure.committed, RENDERED_CONTENT_UNUSABLE_COMMITTED_LIMIT);
|
|
6434
|
+
this.measure.committed.add(key);
|
|
6008
6435
|
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6436
|
+
const stats = new RenderedContentUnusableStats()
|
|
6437
|
+
.setMessageId(params.messageId)
|
|
6438
|
+
.setContentKind(params.contentKind)
|
|
6439
|
+
.setErrorDomain(params.errorDomain)
|
|
6440
|
+
.setError(params.error ? { code: params.error.code, message: params.errorDomain } : undefined)
|
|
6441
|
+
.setCommitCallback(this.commitCallback);
|
|
6442
|
+
|
|
6443
|
+
stats.commit();
|
|
6016
6444
|
}
|
|
6017
6445
|
|
|
6018
6446
|
clear(): void {
|
|
6019
|
-
this.committed.clear();
|
|
6447
|
+
this.measure.committed.clear();
|
|
6020
6448
|
}
|
|
6021
6449
|
}
|
|
6022
6450
|
|
|
6451
|
+
export declare interface RequestNudgeMessageParams {
|
|
6452
|
+
/** Event key that triggers the nudge message. */
|
|
6453
|
+
key: string;
|
|
6454
|
+
/** Additional context for the server to generate the message. */
|
|
6455
|
+
context?: Record<string, string>;
|
|
6456
|
+
}
|
|
6457
|
+
|
|
6023
6458
|
declare interface ResolvedHandlers extends AgentClientHandlers {
|
|
6024
6459
|
onClickLink: (params: {
|
|
6025
6460
|
url: string;
|
|
@@ -6059,17 +6494,95 @@ declare type SingleSelectField = {
|
|
|
6059
6494
|
layout: 'default';
|
|
6060
6495
|
};
|
|
6061
6496
|
|
|
6062
|
-
declare
|
|
6497
|
+
declare abstract class SpanStatsTrackerBase<
|
|
6498
|
+
TSpan extends { onVisibilityChanged(visible: boolean): void },
|
|
6499
|
+
> extends StatsTrackerBase {
|
|
6500
|
+
protected activeSpan: TSpan | null = null;
|
|
6501
|
+
// Screen visibility is cross-attempt state (it outlives supersessions), so the
|
|
6502
|
+
// tracker owns it and stamps it into each new span, like presentMethod.
|
|
6503
|
+
protected visible: boolean = true;
|
|
6063
6504
|
|
|
6064
|
-
|
|
6065
|
-
|
|
6505
|
+
setVisible(visible: boolean): void {
|
|
6506
|
+
this.visible = visible;
|
|
6507
|
+
this.activeSpan?.onVisibilityChanged(visible);
|
|
6508
|
+
}
|
|
6066
6509
|
}
|
|
6067
6510
|
|
|
6068
|
-
declare type
|
|
6069
|
-
|
|
6070
|
-
|
|
6511
|
+
declare type StatsAppendCallback = (type: string, data: AIAgentStatPayload) => boolean;
|
|
6512
|
+
|
|
6513
|
+
declare type StatsPayloadParams = {
|
|
6514
|
+
conversationId?: number;
|
|
6515
|
+
extra?: Record<string, unknown>;
|
|
6071
6516
|
};
|
|
6072
6517
|
|
|
6518
|
+
declare type StatsRecord<TStats, TMeasure> = { stats: TStats; measure: TMeasure };
|
|
6519
|
+
|
|
6520
|
+
/**
|
|
6521
|
+
* One measurement attempt issued by a tracker (`startSpan`), reified as a handle.
|
|
6522
|
+
*
|
|
6523
|
+
* A span is a StatsRecord with lifecycle: `stats` is the metric instance and `measure`
|
|
6524
|
+
* is the tracking metadata, exactly like a keyed record in MultiStatsTrackerBase. The
|
|
6525
|
+
* two idioms differ only in how the record is held — looked up by an entity key versus
|
|
6526
|
+
* captured as a handle at the moment the measured work starts, so late async events
|
|
6527
|
+
* land on the span that owns them instead of whichever measurement is currently active.
|
|
6528
|
+
*
|
|
6529
|
+
* Cancelling severs the metric reference: a superseded or abandoned span becomes
|
|
6530
|
+
* non-recording and can never emit, without any caller-side staleness guards.
|
|
6531
|
+
*
|
|
6532
|
+
* Emission is visibility-gated: the measurement runs regardless of screen
|
|
6533
|
+
* visibility, but a completion reached while hidden (commitOrDefer) is held and
|
|
6534
|
+
* flushed by onVisibilityChanged when the screen becomes visible.
|
|
6535
|
+
*/
|
|
6536
|
+
declare abstract class StatsSpanBase<TStats extends AIAgentBaseStats, TMeasure> {
|
|
6537
|
+
protected stats: TStats | null;
|
|
6538
|
+
protected abstract measure: TMeasure;
|
|
6539
|
+
private visible: boolean;
|
|
6540
|
+
private completedHidden = false;
|
|
6541
|
+
|
|
6542
|
+
constructor(stats: TStats | null, visible: boolean) {
|
|
6543
|
+
this.stats = stats;
|
|
6544
|
+
this.visible = visible;
|
|
6545
|
+
}
|
|
6546
|
+
|
|
6547
|
+
get isRecording(): boolean {
|
|
6548
|
+
return this.stats !== null && !this.stats.isCommitted();
|
|
6549
|
+
}
|
|
6550
|
+
|
|
6551
|
+
onVisibilityChanged(visible: boolean): void {
|
|
6552
|
+
if (!this.stats) return;
|
|
6553
|
+
|
|
6554
|
+
this.visible = visible;
|
|
6555
|
+
if (visible && this.completedHidden) {
|
|
6556
|
+
this.stats.commit();
|
|
6557
|
+
this.stats = null;
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
|
|
6561
|
+
/** Commit now when the screen is visible; otherwise hold the frozen measurement until it becomes visible. */
|
|
6562
|
+
protected commitOrDefer(): void {
|
|
6563
|
+
if (!this.stats) return;
|
|
6564
|
+
|
|
6565
|
+
if (!this.visible) {
|
|
6566
|
+
this.completedHidden = true;
|
|
6567
|
+
return;
|
|
6568
|
+
}
|
|
6569
|
+
|
|
6570
|
+
this.stats.commit();
|
|
6571
|
+
this.stats = null;
|
|
6572
|
+
}
|
|
6573
|
+
|
|
6574
|
+
cancel(): void {
|
|
6575
|
+
this.stats = null;
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
declare abstract class StatsTrackerBase {
|
|
6580
|
+
protected readonly commitCallback: StatsAppendCallback;
|
|
6581
|
+
constructor(commitCallback: StatsAppendCallback) {
|
|
6582
|
+
this.commitCallback = commitCallback;
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
|
|
6073
6586
|
/** Per-event override for the platform `announceStatus` wrappers — same shape as `AnnounceOptions`. */
|
|
6074
6587
|
declare type StatusAnnouncementOptions = AnnounceOptions;
|
|
6075
6588
|
|
|
@@ -6123,20 +6636,75 @@ declare enum StewardTaskStatus {
|
|
|
6123
6636
|
CANCELED = 'CANCELED',
|
|
6124
6637
|
}
|
|
6125
6638
|
|
|
6126
|
-
declare
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6639
|
+
declare type StreamMeasure = {
|
|
6640
|
+
channelUrl?: string;
|
|
6641
|
+
deliveryTimer: TimeoutId | null;
|
|
6642
|
+
lastRawAt: number;
|
|
6643
|
+
lastRawLength: number;
|
|
6644
|
+
lastRenderAt: number;
|
|
6645
|
+
lastRenderedLength: number;
|
|
6646
|
+
renderTimer: TimeoutId | null;
|
|
6647
|
+
};
|
|
6648
|
+
|
|
6649
|
+
declare class StreamStalledStats extends AIAgentBaseStats {
|
|
6650
|
+
private durationMs?: number;
|
|
6651
|
+
private messageId?: number;
|
|
6652
|
+
private stallKind?: StreamStallKind;
|
|
6653
|
+
|
|
6654
|
+
setMessageId(messageId: number): this {
|
|
6655
|
+
this.messageId = messageId;
|
|
6656
|
+
return this;
|
|
6657
|
+
}
|
|
6658
|
+
|
|
6659
|
+
setStall(params: { stallKind: StreamStallKind; durationMs: number }): this {
|
|
6660
|
+
this.durationMs = params.durationMs;
|
|
6661
|
+
this.stallKind = params.stallKind;
|
|
6662
|
+
return this;
|
|
6663
|
+
}
|
|
6664
|
+
|
|
6665
|
+
protected getMetricKey(): string {
|
|
6666
|
+
return METRIC_KEY_STREAM_STALLED;
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
protected buildPayload(): AIAgentStatPayload | null {
|
|
6670
|
+
if (this.durationMs === undefined || this.messageId === undefined || this.stallKind === undefined) return null;
|
|
6133
6671
|
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6672
|
+
return this.createPayload(this.durationMs, {
|
|
6673
|
+
extra: {
|
|
6674
|
+
message_id: String(this.messageId),
|
|
6675
|
+
stall_kind: this.stallKind,
|
|
6676
|
+
},
|
|
6677
|
+
});
|
|
6139
6678
|
}
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
/**
|
|
6682
|
+
* Spec: stream_stalled
|
|
6683
|
+
*
|
|
6684
|
+
* Detects AI response stream stalls and classifies the stalled area. Tracking
|
|
6685
|
+
* is keyed by message_id because multiple AI response messages can be active
|
|
6686
|
+
* independently.
|
|
6687
|
+
*
|
|
6688
|
+
* Timeline:
|
|
6689
|
+
* - Start when a streaming AI response produces raw data for a message_id.
|
|
6690
|
+
* - delivery stall: raw response data does not advance for 10s.
|
|
6691
|
+
* - render stall: raw response data is ahead/arriving, but visible render
|
|
6692
|
+
* progress does not advance for 5s.
|
|
6693
|
+
* - Raw progress refreshes the delivery checkpoint; rendered progress refreshes
|
|
6694
|
+
* the render checkpoint.
|
|
6695
|
+
*
|
|
6696
|
+
* Payload:
|
|
6697
|
+
* - value: stall_duration_ms.
|
|
6698
|
+
* - extra.stall_kind: delivery | render.
|
|
6699
|
+
* - extra.message_id: tracked AI response message id.
|
|
6700
|
+
*
|
|
6701
|
+
* Policy:
|
|
6702
|
+
* - Message-scoped; first stall only per message_id.
|
|
6703
|
+
* - Delivery and render stalls are mutually exclusive for the same turn.
|
|
6704
|
+
* - A short response completing before either threshold is not recorded.
|
|
6705
|
+
*/
|
|
6706
|
+
declare class StreamStalledStatsTracker extends MultiStatsTrackerBase<StreamStalledStats, StreamMeasure> {
|
|
6707
|
+
private readonly emittedMessageChannels = new Map<number, string | undefined>();
|
|
6140
6708
|
|
|
6141
6709
|
markRawProgress(params: {
|
|
6142
6710
|
messageId: number;
|
|
@@ -6151,54 +6719,52 @@ declare class StreamStalledStatsTracker {
|
|
|
6151
6719
|
|
|
6152
6720
|
if (this.emittedMessageChannels.has(params.messageId)) return;
|
|
6153
6721
|
|
|
6154
|
-
const
|
|
6155
|
-
|
|
6156
|
-
if (params.textLength > stream.lastRawLength) {
|
|
6157
|
-
stream.chunksReceived += 1;
|
|
6158
|
-
stream.lastRawLength = params.textLength;
|
|
6159
|
-
stream.lastRawAt = this.clock.now();
|
|
6722
|
+
const record = this.getOrCreate(params.messageId, params.channelUrl);
|
|
6723
|
+
const measure = record.measure;
|
|
6160
6724
|
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6725
|
+
if (this.markRawProgressForRecord(record, params.textLength, performance.now())) {
|
|
6726
|
+
if (measure.deliveryTimer !== null) clearTimeout(measure.deliveryTimer);
|
|
6727
|
+
measure.deliveryTimer = null;
|
|
6728
|
+
this.scheduleDeliveryStallCheck(params.messageId, record);
|
|
6164
6729
|
} else {
|
|
6165
|
-
this.scheduleDeliveryStallCheck(params.messageId,
|
|
6730
|
+
this.scheduleDeliveryStallCheck(params.messageId, record);
|
|
6166
6731
|
}
|
|
6167
6732
|
|
|
6168
|
-
if (
|
|
6169
|
-
|
|
6170
|
-
this.scheduleRenderStallCheck(params.messageId,
|
|
6733
|
+
if (this.isRenderPending(record) && measure.renderTimer === null) {
|
|
6734
|
+
measure.lastRenderAt = performance.now();
|
|
6735
|
+
this.scheduleRenderStallCheck(params.messageId, record, STREAM_RENDER_STALL_THRESHOLD_MS);
|
|
6171
6736
|
}
|
|
6172
6737
|
}
|
|
6173
6738
|
|
|
6174
6739
|
markRenderedProgress(params: { messageId: number; displayedLength: number }): void {
|
|
6175
|
-
const
|
|
6176
|
-
if (!
|
|
6740
|
+
const record = this.records.get(params.messageId);
|
|
6741
|
+
if (!record) return;
|
|
6742
|
+
const measure = record.measure;
|
|
6177
6743
|
|
|
6178
|
-
|
|
6179
|
-
|
|
6744
|
+
const progressed = this.markRenderedProgressForRecord(record, params.displayedLength, performance.now());
|
|
6745
|
+
if (!progressed) return;
|
|
6180
6746
|
|
|
6181
|
-
if (
|
|
6182
|
-
if (
|
|
6183
|
-
this.
|
|
6747
|
+
if (!this.isRenderPending(record)) {
|
|
6748
|
+
if (measure.deliveryTimer === null) {
|
|
6749
|
+
this.clearRecord(params.messageId, record);
|
|
6184
6750
|
return;
|
|
6185
6751
|
}
|
|
6186
6752
|
|
|
6187
|
-
|
|
6188
|
-
|
|
6753
|
+
if (measure.renderTimer !== null) clearTimeout(measure.renderTimer);
|
|
6754
|
+
measure.renderTimer = null;
|
|
6189
6755
|
return;
|
|
6190
6756
|
}
|
|
6191
6757
|
|
|
6192
|
-
this.scheduleRenderStallCheck(params.messageId,
|
|
6758
|
+
this.scheduleRenderStallCheck(params.messageId, record, STREAM_RENDER_STALL_THRESHOLD_MS);
|
|
6193
6759
|
}
|
|
6194
6760
|
|
|
6195
6761
|
hasPendingStream(messageId: number): boolean {
|
|
6196
|
-
return this.
|
|
6762
|
+
return this.records.has(messageId);
|
|
6197
6763
|
}
|
|
6198
6764
|
|
|
6199
6765
|
complete(messageId: number): void {
|
|
6200
|
-
const
|
|
6201
|
-
if (
|
|
6766
|
+
const record = this.records.get(messageId);
|
|
6767
|
+
if (record) this.clearRecord(messageId, record);
|
|
6202
6768
|
}
|
|
6203
6769
|
|
|
6204
6770
|
// Final stream update: no more raw chunks are coming, so stop delivery-stall
|
|
@@ -6208,52 +6774,53 @@ declare class StreamStalledStatsTracker {
|
|
|
6208
6774
|
// raw length — otherwise a renderer that freezes mid-catch-up never emits a
|
|
6209
6775
|
// 'render' stall.
|
|
6210
6776
|
private completeDelivery(messageId: number, finalRawLength: number): void {
|
|
6211
|
-
const
|
|
6212
|
-
if (!
|
|
6777
|
+
const record = this.records.get(messageId);
|
|
6778
|
+
if (!record) return;
|
|
6779
|
+
const measure = record.measure;
|
|
6213
6780
|
|
|
6214
6781
|
// The final stream:false payload can carry text beyond the last streamed
|
|
6215
6782
|
// chunk; fold it into the raw length first so the render watch targets the
|
|
6216
6783
|
// full text the animation still has to reveal (otherwise the stream could be
|
|
6217
6784
|
// cleared while a freeze on that trailing text would go unreported).
|
|
6218
|
-
if (finalRawLength >
|
|
6219
|
-
|
|
6220
|
-
|
|
6785
|
+
if (finalRawLength > measure.lastRawLength) {
|
|
6786
|
+
measure.lastRawLength = finalRawLength;
|
|
6787
|
+
measure.lastRawAt = performance.now();
|
|
6221
6788
|
}
|
|
6222
6789
|
|
|
6223
|
-
|
|
6224
|
-
|
|
6790
|
+
if (measure.deliveryTimer !== null) clearTimeout(measure.deliveryTimer);
|
|
6791
|
+
measure.deliveryTimer = null;
|
|
6225
6792
|
|
|
6226
|
-
if (
|
|
6227
|
-
this.
|
|
6793
|
+
if (!this.isRenderPending(record)) {
|
|
6794
|
+
this.clearRecord(messageId, record);
|
|
6228
6795
|
return;
|
|
6229
6796
|
}
|
|
6230
6797
|
|
|
6231
|
-
if (
|
|
6232
|
-
|
|
6233
|
-
this.scheduleRenderStallCheck(messageId,
|
|
6798
|
+
if (measure.renderTimer === null) {
|
|
6799
|
+
measure.lastRenderAt = performance.now();
|
|
6800
|
+
this.scheduleRenderStallCheck(messageId, record, STREAM_RENDER_STALL_THRESHOLD_MS);
|
|
6234
6801
|
}
|
|
6235
6802
|
}
|
|
6236
6803
|
|
|
6237
6804
|
clearMessage(messageId: number): void {
|
|
6238
|
-
const
|
|
6239
|
-
if (
|
|
6805
|
+
const record = this.records.get(messageId);
|
|
6806
|
+
if (record) this.clearRecord(messageId, record);
|
|
6240
6807
|
this.emittedMessageChannels.delete(messageId);
|
|
6241
6808
|
}
|
|
6242
6809
|
|
|
6243
6810
|
clear(): void {
|
|
6244
|
-
this.
|
|
6245
|
-
|
|
6246
|
-
|
|
6811
|
+
this.records.forEach((record) => {
|
|
6812
|
+
if (record.measure.deliveryTimer !== null) clearTimeout(record.measure.deliveryTimer);
|
|
6813
|
+
if (record.measure.renderTimer !== null) clearTimeout(record.measure.renderTimer);
|
|
6247
6814
|
});
|
|
6248
|
-
this.
|
|
6815
|
+
this.records.clear();
|
|
6249
6816
|
this.emittedMessageChannels.clear();
|
|
6250
6817
|
}
|
|
6251
6818
|
|
|
6252
6819
|
clearChannel(channelUrl?: string): void {
|
|
6253
6820
|
if (!channelUrl) return;
|
|
6254
|
-
this.
|
|
6255
|
-
if (
|
|
6256
|
-
this.
|
|
6821
|
+
this.records.forEach((record, messageId) => {
|
|
6822
|
+
if (record.measure.channelUrl !== channelUrl && record.measure.channelUrl !== undefined) return;
|
|
6823
|
+
this.clearRecord(messageId, record);
|
|
6257
6824
|
});
|
|
6258
6825
|
this.emittedMessageChannels.forEach((emittedChannelUrl, messageId) => {
|
|
6259
6826
|
if (emittedChannelUrl === channelUrl || emittedChannelUrl === undefined)
|
|
@@ -6262,83 +6829,95 @@ declare class StreamStalledStatsTracker {
|
|
|
6262
6829
|
}
|
|
6263
6830
|
|
|
6264
6831
|
private detectDelivery(messageId: number): void {
|
|
6265
|
-
const
|
|
6266
|
-
if (!
|
|
6267
|
-
|
|
6832
|
+
const record = this.records.get(messageId);
|
|
6833
|
+
if (!record) return;
|
|
6834
|
+
const measure = record.measure;
|
|
6835
|
+
measure.deliveryTimer = null;
|
|
6268
6836
|
// Mutex: cancel any in-flight render stall check so delivery wins this turn.
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
this.emit(messageId,
|
|
6837
|
+
if (measure.renderTimer !== null) clearTimeout(measure.renderTimer);
|
|
6838
|
+
measure.renderTimer = null;
|
|
6839
|
+
this.emit(messageId, record, 'delivery');
|
|
6272
6840
|
}
|
|
6273
6841
|
|
|
6274
6842
|
private detectRender(messageId: number): void {
|
|
6275
|
-
const
|
|
6276
|
-
if (!
|
|
6277
|
-
|
|
6278
|
-
|
|
6843
|
+
const record = this.records.get(messageId);
|
|
6844
|
+
if (!record) return;
|
|
6845
|
+
const measure = record.measure;
|
|
6846
|
+
measure.renderTimer = null;
|
|
6847
|
+
if (!this.isRenderPending(record)) return;
|
|
6279
6848
|
|
|
6280
|
-
const elapsedSinceRender =
|
|
6849
|
+
const elapsedSinceRender = performance.now() - measure.lastRenderAt;
|
|
6281
6850
|
if (elapsedSinceRender < STREAM_RENDER_STALL_THRESHOLD_MS) {
|
|
6282
|
-
this.scheduleRenderStallCheck(messageId,
|
|
6851
|
+
this.scheduleRenderStallCheck(messageId, record, STREAM_RENDER_STALL_THRESHOLD_MS - elapsedSinceRender);
|
|
6283
6852
|
return;
|
|
6284
6853
|
}
|
|
6285
6854
|
|
|
6286
6855
|
// Mutex rule: when raw progress has been silent for at least the render threshold,
|
|
6287
6856
|
// delivery is the root cause and will win. Defer the render emit until delivery has
|
|
6288
6857
|
// had a chance to fire so we don't surface render as the symptom.
|
|
6289
|
-
const elapsedSinceRaw =
|
|
6290
|
-
if (elapsedSinceRaw >= STREAM_RENDER_STALL_THRESHOLD_MS &&
|
|
6858
|
+
const elapsedSinceRaw = performance.now() - measure.lastRawAt;
|
|
6859
|
+
if (elapsedSinceRaw >= STREAM_RENDER_STALL_THRESHOLD_MS && measure.deliveryTimer !== null) {
|
|
6291
6860
|
const untilDelivery = STREAM_DELIVERY_STALL_THRESHOLD_MS - elapsedSinceRaw;
|
|
6292
6861
|
if (untilDelivery > 0) {
|
|
6293
|
-
this.scheduleRenderStallCheck(messageId,
|
|
6862
|
+
this.scheduleRenderStallCheck(messageId, record, untilDelivery);
|
|
6294
6863
|
return;
|
|
6295
6864
|
}
|
|
6296
6865
|
}
|
|
6297
6866
|
|
|
6298
|
-
this.emit(messageId,
|
|
6867
|
+
this.emit(messageId, record, 'render');
|
|
6299
6868
|
}
|
|
6300
6869
|
|
|
6301
|
-
private scheduleRenderStallCheck(
|
|
6302
|
-
|
|
6303
|
-
|
|
6870
|
+
private scheduleRenderStallCheck(
|
|
6871
|
+
messageId: number,
|
|
6872
|
+
record: StatsRecord<StreamStalledStats, StreamMeasure>,
|
|
6873
|
+
delayMs: number,
|
|
6874
|
+
): void {
|
|
6875
|
+
if (record.measure.renderTimer !== null) return;
|
|
6876
|
+
record.measure.renderTimer = setTimeout(() => this.detectRender(messageId), delayMs);
|
|
6304
6877
|
}
|
|
6305
6878
|
|
|
6306
|
-
private scheduleDeliveryStallCheck(messageId: number,
|
|
6307
|
-
if (
|
|
6308
|
-
|
|
6879
|
+
private scheduleDeliveryStallCheck(messageId: number, record: StatsRecord<StreamStalledStats, StreamMeasure>): void {
|
|
6880
|
+
if (record.measure.deliveryTimer !== null) return;
|
|
6881
|
+
record.measure.deliveryTimer = setTimeout(() => this.detectDelivery(messageId), STREAM_DELIVERY_STALL_THRESHOLD_MS);
|
|
6309
6882
|
}
|
|
6310
6883
|
|
|
6311
|
-
private emit(
|
|
6884
|
+
private emit(
|
|
6885
|
+
messageId: number,
|
|
6886
|
+
record: StatsRecord<StreamStalledStats, StreamMeasure>,
|
|
6887
|
+
stallKind: StreamStallKind,
|
|
6888
|
+
): void {
|
|
6312
6889
|
if (this.emittedMessageChannels.has(messageId)) return;
|
|
6313
6890
|
|
|
6314
|
-
const
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
},
|
|
6320
|
-
});
|
|
6321
|
-
this.rememberEmitted(messageId, stream.channelUrl);
|
|
6322
|
-
this.clearStream(messageId, stream);
|
|
6891
|
+
const stalledAt = performance.now();
|
|
6892
|
+
const stallStartedAt = stallKind === 'delivery' ? record.measure.lastRawAt : record.measure.lastRenderAt;
|
|
6893
|
+
record.stats.setStall({ stallKind, durationMs: stalledAt - stallStartedAt }).commit();
|
|
6894
|
+
this.rememberEmitted(messageId, record.measure.channelUrl);
|
|
6895
|
+
this.clearRecord(messageId, record);
|
|
6323
6896
|
}
|
|
6324
6897
|
|
|
6325
|
-
private getOrCreate(
|
|
6326
|
-
|
|
6898
|
+
private getOrCreate(
|
|
6899
|
+
messageId: number,
|
|
6900
|
+
channelUrl: string | undefined,
|
|
6901
|
+
): StatsRecord<StreamStalledStats, StreamMeasure> {
|
|
6902
|
+
const existing = this.records.get(messageId);
|
|
6327
6903
|
if (existing) return existing;
|
|
6328
6904
|
|
|
6329
|
-
const
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6905
|
+
const startedAt = performance.now();
|
|
6906
|
+
|
|
6907
|
+
const record: StatsRecord<StreamStalledStats, StreamMeasure> = {
|
|
6908
|
+
stats: new StreamStalledStats().setMessageId(messageId).setCommitCallback(this.commitCallback),
|
|
6909
|
+
measure: {
|
|
6910
|
+
channelUrl,
|
|
6911
|
+
deliveryTimer: null,
|
|
6912
|
+
lastRawAt: startedAt,
|
|
6913
|
+
lastRawLength: 0,
|
|
6914
|
+
lastRenderAt: startedAt,
|
|
6915
|
+
lastRenderedLength: 0,
|
|
6916
|
+
renderTimer: null,
|
|
6917
|
+
},
|
|
6339
6918
|
};
|
|
6340
|
-
this.
|
|
6341
|
-
return
|
|
6919
|
+
this.records.set(messageId, record);
|
|
6920
|
+
return record;
|
|
6342
6921
|
}
|
|
6343
6922
|
|
|
6344
6923
|
private rememberEmitted(messageId: number, channelUrl: string | undefined): void {
|
|
@@ -6348,26 +6927,43 @@ declare class StreamStalledStatsTracker {
|
|
|
6348
6927
|
this.emittedMessageChannels.set(messageId, channelUrl);
|
|
6349
6928
|
}
|
|
6350
6929
|
|
|
6351
|
-
private
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
this.
|
|
6930
|
+
private clearRecord(messageId: string | number, record: StatsRecord<StreamStalledStats, StreamMeasure>): void {
|
|
6931
|
+
if (record.measure.deliveryTimer !== null) clearTimeout(record.measure.deliveryTimer);
|
|
6932
|
+
if (record.measure.renderTimer !== null) clearTimeout(record.measure.renderTimer);
|
|
6933
|
+
this.records.delete(messageId);
|
|
6934
|
+
}
|
|
6935
|
+
|
|
6936
|
+
private isRenderPending(record: StatsRecord<StreamStalledStats, StreamMeasure>): boolean {
|
|
6937
|
+
return record.measure.lastRenderedLength < record.measure.lastRawLength;
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6940
|
+
private markRawProgressForRecord(
|
|
6941
|
+
record: StatsRecord<StreamStalledStats, StreamMeasure>,
|
|
6942
|
+
textLength: number,
|
|
6943
|
+
rawAt: number,
|
|
6944
|
+
): boolean {
|
|
6945
|
+
if (textLength <= record.measure.lastRawLength) return false;
|
|
6946
|
+
|
|
6947
|
+
record.measure.lastRawLength = textLength;
|
|
6948
|
+
record.measure.lastRawAt = rawAt;
|
|
6949
|
+
return true;
|
|
6950
|
+
}
|
|
6951
|
+
|
|
6952
|
+
private markRenderedProgressForRecord(
|
|
6953
|
+
record: StatsRecord<StreamStalledStats, StreamMeasure>,
|
|
6954
|
+
displayedLength: number,
|
|
6955
|
+
renderedAt: number,
|
|
6956
|
+
): boolean {
|
|
6957
|
+
if (displayedLength <= record.measure.lastRenderedLength) return false;
|
|
6958
|
+
|
|
6959
|
+
record.measure.lastRenderedLength = displayedLength;
|
|
6960
|
+
record.measure.lastRenderAt = renderedAt;
|
|
6961
|
+
return true;
|
|
6355
6962
|
}
|
|
6356
6963
|
}
|
|
6357
6964
|
|
|
6358
6965
|
declare type StreamStallKind = 'delivery' | 'render';
|
|
6359
6966
|
|
|
6360
|
-
declare type StreamState = {
|
|
6361
|
-
channelUrl?: string;
|
|
6362
|
-
chunksReceived: number;
|
|
6363
|
-
lastRawAt: number;
|
|
6364
|
-
lastRenderAt: number;
|
|
6365
|
-
lastRawLength: number;
|
|
6366
|
-
lastRenderedLength: number;
|
|
6367
|
-
deliveryTimer: TimeoutId | null;
|
|
6368
|
-
renderTimer: TimeoutId | null;
|
|
6369
|
-
};
|
|
6370
|
-
|
|
6371
6967
|
/**
|
|
6372
6968
|
* StringSet type for React components
|
|
6373
6969
|
* Uses SNAKE_CASE keys for backward compatibility
|
|
@@ -6513,6 +7109,7 @@ export declare type StringSet = {
|
|
|
6513
7109
|
A11Y_CONVERSATION_ITEM: (agent: string, status: string, time: string, count: number) => string;
|
|
6514
7110
|
A11Y_CONNECT_AGENT: string;
|
|
6515
7111
|
A11Y_CLOSE_MESSENGER: string;
|
|
7112
|
+
A11Y_DISMISS_NUDGE_MESSAGE: string;
|
|
6516
7113
|
A11Y_EXPAND_MESSENGER: string;
|
|
6517
7114
|
A11Y_COLLAPSE_MESSENGER: string;
|
|
6518
7115
|
A11Y_HINT_SEND_MESSAGE: string;
|
|
@@ -6642,11 +7239,6 @@ declare type TextField = {
|
|
|
6642
7239
|
};
|
|
6643
7240
|
};
|
|
6644
7241
|
|
|
6645
|
-
declare type TimedTrackerOptions = TrackerOptions & {
|
|
6646
|
-
setTimeout?: typeof setTimeout;
|
|
6647
|
-
clearTimeout?: typeof clearTimeout;
|
|
6648
|
-
};
|
|
6649
|
-
|
|
6650
7242
|
declare type TimeoutId = ReturnType<typeof setTimeout>;
|
|
6651
7243
|
|
|
6652
7244
|
declare interface TimerData {
|
|
@@ -6654,42 +7246,94 @@ declare interface TimerData {
|
|
|
6654
7246
|
endTime: number | null;
|
|
6655
7247
|
}
|
|
6656
7248
|
|
|
6657
|
-
|
|
6658
|
-
|
|
7249
|
+
/** A never-started timer reads as all-null; duration stays null until the timer stops. */
|
|
7250
|
+
declare type TimerReading = {
|
|
7251
|
+
startAt: number | null;
|
|
7252
|
+
endedAt: number | null;
|
|
7253
|
+
duration: number | null;
|
|
6659
7254
|
};
|
|
6660
7255
|
|
|
6661
|
-
declare class
|
|
6662
|
-
private
|
|
6663
|
-
private
|
|
6664
|
-
private readonly setTimeoutFn: typeof setTimeout;
|
|
6665
|
-
private readonly clearTimeoutFn: typeof clearTimeout;
|
|
6666
|
-
private readonly turns = new Map<string, TypingTurn>();
|
|
6667
|
-
private readonly pendingAIResponseStartsByUserMessageId = new Map<number, PendingAIResponseStart>();
|
|
7256
|
+
declare class TypingIndicatorAbsentTimeoutStats extends AIAgentBaseStats {
|
|
7257
|
+
private beforeTpstAbsentMs?: number;
|
|
7258
|
+
private afterTpenAbsentMs?: number;
|
|
6668
7259
|
|
|
6669
|
-
|
|
6670
|
-
this.
|
|
6671
|
-
this
|
|
6672
|
-
this.setTimeoutFn = getSetTimeout(options);
|
|
6673
|
-
this.clearTimeoutFn = getClearTimeout(options);
|
|
7260
|
+
setBeforeTpstAbsentMs(duration: number): this {
|
|
7261
|
+
this.beforeTpstAbsentMs = duration;
|
|
7262
|
+
return this;
|
|
6674
7263
|
}
|
|
6675
7264
|
|
|
7265
|
+
setAfterTpenAbsentMs(duration: number): this {
|
|
7266
|
+
this.afterTpenAbsentMs = duration;
|
|
7267
|
+
return this;
|
|
7268
|
+
}
|
|
7269
|
+
|
|
7270
|
+
protected getMetricKey(): string {
|
|
7271
|
+
return METRIC_KEY_AI_TYPING_INDICATOR_ABSENT_TIMEOUT;
|
|
7272
|
+
}
|
|
7273
|
+
|
|
7274
|
+
protected buildPayload(): AIAgentStatPayload | null {
|
|
7275
|
+
if (this.beforeTpstAbsentMs === undefined && this.afterTpenAbsentMs === undefined) return null;
|
|
7276
|
+
|
|
7277
|
+
return this.createPayload((this.beforeTpstAbsentMs ?? 0) + (this.afterTpenAbsentMs ?? 0), {
|
|
7278
|
+
extra: {
|
|
7279
|
+
[DurationKey.BEFORE_TPST_ABSENT]: this.beforeTpstAbsentMs,
|
|
7280
|
+
[DurationKey.AFTER_TPEN_ABSENT]: this.afterTpenAbsentMs,
|
|
7281
|
+
},
|
|
7282
|
+
});
|
|
7283
|
+
}
|
|
7284
|
+
}
|
|
7285
|
+
|
|
7286
|
+
/**
|
|
7287
|
+
* Spec: ai_typing_indicator_absent_timeout
|
|
7288
|
+
*
|
|
7289
|
+
* Detects server typing signal gaps during an AI turn. The metric is emitted
|
|
7290
|
+
* only for AI conversations and tracks two absent phases in the same turn.
|
|
7291
|
+
*
|
|
7292
|
+
* Timeline:
|
|
7293
|
+
* - Start before-TPST measurement when the user message send turn starts.
|
|
7294
|
+
* - Close before-TPST when TPST/typing-start is observed.
|
|
7295
|
+
* - Start after-TPEN measurement when TPEN/typing-end is observed.
|
|
7296
|
+
* - Close after-TPEN when the AI response message starts streaming.
|
|
7297
|
+
* - Commit when either phase exceeds the 3s absent threshold, or when the
|
|
7298
|
+
* 30s collection safety timeout is reached.
|
|
7299
|
+
*
|
|
7300
|
+
* Payload:
|
|
7301
|
+
* - value: absent_duration_ms, the total measured absent duration.
|
|
7302
|
+
* - extra.before_tpst_absent_ms: gap before TPST, when present.
|
|
7303
|
+
* - extra.after_tpen_absent_ms: gap after TPEN, when present.
|
|
7304
|
+
*
|
|
7305
|
+
* Policy:
|
|
7306
|
+
* - Turn-scoped; each user message turn is measured independently.
|
|
7307
|
+
* - The Phase 2 example sums both measured absent phases in value, even when
|
|
7308
|
+
* only one phase reaches the threshold.
|
|
7309
|
+
* - Human-agent handoff/abort clears the pending turn without emitting.
|
|
7310
|
+
*/
|
|
7311
|
+
declare class TypingIndicatorAbsentTimeoutStatsTracker extends MultiStatsTrackerBase<
|
|
7312
|
+
TypingIndicatorAbsentTimeoutStats,
|
|
7313
|
+
TypingMeasure
|
|
7314
|
+
> {
|
|
7315
|
+
private readonly pendingAIResponseStartsByUserMessageId = new Map<number, PendingAIResponseStart>();
|
|
7316
|
+
|
|
6676
7317
|
startTurn(params: { requestId: string; channelUrl?: string }): void {
|
|
6677
7318
|
this.cancelTurn(params.requestId);
|
|
6678
|
-
const
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
7319
|
+
const record: StatsRecord<TypingIndicatorAbsentTimeoutStats, TypingMeasure> = {
|
|
7320
|
+
stats: new TypingIndicatorAbsentTimeoutStats().setCommitCallback(this.commitCallback),
|
|
7321
|
+
measure: {
|
|
7322
|
+
afterTpenStartedAt: null,
|
|
7323
|
+
beforeTpstStartedAt: performance.now(),
|
|
7324
|
+
channelUrl: params.channelUrl,
|
|
7325
|
+
requestId: params.requestId,
|
|
7326
|
+
timer: null,
|
|
7327
|
+
},
|
|
6684
7328
|
};
|
|
6685
|
-
|
|
6686
|
-
this.
|
|
7329
|
+
record.measure.timer = setTimeout(() => this.commitTimeout(record.measure.requestId), TYPING_COLLECTION_TIMEOUT_MS);
|
|
7330
|
+
this.records.set(params.requestId, record);
|
|
6687
7331
|
}
|
|
6688
7332
|
|
|
6689
7333
|
markUserMessageAck(params: { requestId: string; messageId: number }): void {
|
|
6690
|
-
const
|
|
6691
|
-
if (!
|
|
6692
|
-
|
|
7334
|
+
const record = this.records.get(params.requestId);
|
|
7335
|
+
if (!record) return;
|
|
7336
|
+
record.measure.userMessageId = params.messageId;
|
|
6693
7337
|
|
|
6694
7338
|
const pending = this.pendingAIResponseStartsByUserMessageId.get(params.messageId);
|
|
6695
7339
|
if (!pending) return;
|
|
@@ -6703,12 +7347,12 @@ declare class TypingIndicatorAbsentTimeoutStatsTracker {
|
|
|
6703
7347
|
replaceTurnRequestId(currentRequestId: string, nextRequestId: string): void {
|
|
6704
7348
|
if (currentRequestId === nextRequestId) return;
|
|
6705
7349
|
|
|
6706
|
-
const
|
|
6707
|
-
if (!
|
|
7350
|
+
const record = this.records.get(currentRequestId);
|
|
7351
|
+
if (!record) return;
|
|
6708
7352
|
|
|
6709
|
-
this.
|
|
6710
|
-
|
|
6711
|
-
this.
|
|
7353
|
+
this.records.delete(currentRequestId);
|
|
7354
|
+
record.measure.requestId = nextRequestId;
|
|
7355
|
+
this.records.set(nextRequestId, record);
|
|
6712
7356
|
}
|
|
6713
7357
|
|
|
6714
7358
|
cancelTurn(requestId: string): void {
|
|
@@ -6716,20 +7360,20 @@ declare class TypingIndicatorAbsentTimeoutStatsTracker {
|
|
|
6716
7360
|
}
|
|
6717
7361
|
|
|
6718
7362
|
markTypingStarted(channelUrl?: string): void {
|
|
6719
|
-
const now =
|
|
6720
|
-
this.
|
|
6721
|
-
if (!matchesChannel(
|
|
6722
|
-
this.recordAfterTpen(
|
|
6723
|
-
this.recordBeforeTpst(
|
|
7363
|
+
const now = performance.now();
|
|
7364
|
+
this.records.forEach((record) => {
|
|
7365
|
+
if (!matchesChannel(record.measure.channelUrl, channelUrl)) return;
|
|
7366
|
+
this.recordAfterTpen(record, now);
|
|
7367
|
+
this.recordBeforeTpst(record, now);
|
|
6724
7368
|
});
|
|
6725
7369
|
}
|
|
6726
7370
|
|
|
6727
7371
|
markTypingEnded(channelUrl?: string): void {
|
|
6728
|
-
const now =
|
|
6729
|
-
this.
|
|
6730
|
-
if (!matchesChannel(
|
|
6731
|
-
this.recordBeforeTpst(
|
|
6732
|
-
|
|
7372
|
+
const now = performance.now();
|
|
7373
|
+
this.records.forEach((record) => {
|
|
7374
|
+
if (!matchesChannel(record.measure.channelUrl, channelUrl)) return;
|
|
7375
|
+
this.recordBeforeTpst(record, now);
|
|
7376
|
+
record.measure.afterTpenStartedAt = now;
|
|
6733
7377
|
});
|
|
6734
7378
|
}
|
|
6735
7379
|
|
|
@@ -6737,9 +7381,15 @@ declare class TypingIndicatorAbsentTimeoutStatsTracker {
|
|
|
6737
7381
|
const channelUrl = params.channelUrl;
|
|
6738
7382
|
const userMessageId = params.userMessageId;
|
|
6739
7383
|
const matchesUserMessage = userMessageId !== undefined;
|
|
6740
|
-
const now =
|
|
7384
|
+
const now = performance.now();
|
|
6741
7385
|
const matched = this.applyAIResponseStarted({ channelUrl, matchesUserMessage, userMessageId }, now);
|
|
6742
|
-
if (
|
|
7386
|
+
if (
|
|
7387
|
+
matched ||
|
|
7388
|
+
!matchesUserMessage ||
|
|
7389
|
+
userMessageId === undefined ||
|
|
7390
|
+
!hasUnackedChannelTurn(this.records.values(), channelUrl)
|
|
7391
|
+
)
|
|
7392
|
+
return;
|
|
6743
7393
|
|
|
6744
7394
|
if (!this.pendingAIResponseStartsByUserMessageId.has(userMessageId)) {
|
|
6745
7395
|
evictOldestIfAtLimit(this.pendingAIResponseStartsByUserMessageId, 100);
|
|
@@ -6754,16 +7404,16 @@ declare class TypingIndicatorAbsentTimeoutStatsTracker {
|
|
|
6754
7404
|
if (!params.matchesUserMessage) {
|
|
6755
7405
|
const match = this.findSingleTurn(params.channelUrl);
|
|
6756
7406
|
if (!match) return false;
|
|
6757
|
-
this.completeTurn(match.requestId, match.
|
|
7407
|
+
this.completeTurn(match.requestId, match.record, startedAt);
|
|
6758
7408
|
return true;
|
|
6759
7409
|
}
|
|
6760
7410
|
|
|
6761
7411
|
let matched = false;
|
|
6762
|
-
this.
|
|
6763
|
-
if (!matchesChannel(
|
|
6764
|
-
if (params.matchesUserMessage &&
|
|
7412
|
+
this.records.forEach((record) => {
|
|
7413
|
+
if (!matchesChannel(record.measure.channelUrl, params.channelUrl)) return;
|
|
7414
|
+
if (params.matchesUserMessage && record.measure.userMessageId !== params.userMessageId) return;
|
|
6765
7415
|
matched = true;
|
|
6766
|
-
this.completeTurn(requestId,
|
|
7416
|
+
this.completeTurn(record.measure.requestId, record, startedAt);
|
|
6767
7417
|
});
|
|
6768
7418
|
return matched;
|
|
6769
7419
|
}
|
|
@@ -6771,38 +7421,38 @@ declare class TypingIndicatorAbsentTimeoutStatsTracker {
|
|
|
6771
7421
|
private findSingleTurn(channelUrl?: string): TypingTurnEntry | undefined {
|
|
6772
7422
|
let match: TypingTurnEntry | undefined;
|
|
6773
7423
|
let matchedCount = 0;
|
|
6774
|
-
this.
|
|
6775
|
-
if (!matchesChannel(
|
|
7424
|
+
this.records.forEach((record) => {
|
|
7425
|
+
if (!matchesChannel(record.measure.channelUrl, channelUrl)) return;
|
|
6776
7426
|
matchedCount += 1;
|
|
6777
7427
|
if (matchedCount > 1) return;
|
|
6778
|
-
match = { requestId,
|
|
7428
|
+
match = { requestId: record.measure.requestId, record };
|
|
6779
7429
|
});
|
|
6780
7430
|
return matchedCount === 1 ? match : undefined;
|
|
6781
7431
|
}
|
|
6782
7432
|
|
|
6783
|
-
private completeTurn(
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
7433
|
+
private completeTurn(
|
|
7434
|
+
requestId: string,
|
|
7435
|
+
record: StatsRecord<TypingIndicatorAbsentTimeoutStats, TypingMeasure>,
|
|
7436
|
+
startedAt: number,
|
|
7437
|
+
): void {
|
|
7438
|
+
this.recordBeforeTpst(record, startedAt);
|
|
7439
|
+
this.recordAfterTpen(record, startedAt);
|
|
7440
|
+
this.commitIfAbsent(requestId, record);
|
|
6791
7441
|
}
|
|
6792
7442
|
|
|
6793
7443
|
clear(): void {
|
|
6794
|
-
this.
|
|
6795
|
-
|
|
7444
|
+
this.records.forEach((record) => {
|
|
7445
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
6796
7446
|
});
|
|
6797
|
-
this.
|
|
7447
|
+
this.records.clear();
|
|
6798
7448
|
this.pendingAIResponseStartsByUserMessageId.clear();
|
|
6799
7449
|
}
|
|
6800
7450
|
|
|
6801
7451
|
clearChannel(channelUrl?: string): void {
|
|
6802
7452
|
if (!channelUrl) return;
|
|
6803
|
-
this.
|
|
6804
|
-
if (
|
|
6805
|
-
this.cleanupTurn(requestId);
|
|
7453
|
+
this.records.forEach((record) => {
|
|
7454
|
+
if (record.measure.channelUrl !== channelUrl) return;
|
|
7455
|
+
this.cleanupTurn(record.measure.requestId);
|
|
6806
7456
|
});
|
|
6807
7457
|
this.pendingAIResponseStartsByUserMessageId.forEach((pending, userMessageId) => {
|
|
6808
7458
|
if (pending.channelUrl === channelUrl) this.pendingAIResponseStartsByUserMessageId.delete(userMessageId);
|
|
@@ -6810,78 +7460,62 @@ declare class TypingIndicatorAbsentTimeoutStatsTracker {
|
|
|
6810
7460
|
}
|
|
6811
7461
|
|
|
6812
7462
|
private commitTimeout(requestId: string): void {
|
|
6813
|
-
const
|
|
6814
|
-
if (!
|
|
6815
|
-
const now =
|
|
6816
|
-
this.recordBeforeTpst(
|
|
6817
|
-
this.recordAfterTpen(
|
|
6818
|
-
this.commitIfAbsent(requestId,
|
|
7463
|
+
const record = this.records.get(requestId);
|
|
7464
|
+
if (!record) return;
|
|
7465
|
+
const now = performance.now();
|
|
7466
|
+
this.recordBeforeTpst(record, now);
|
|
7467
|
+
this.recordAfterTpen(record, now);
|
|
7468
|
+
this.commitIfAbsent(requestId, record);
|
|
7469
|
+
}
|
|
7470
|
+
|
|
7471
|
+
private commitIfAbsent(
|
|
7472
|
+
requestId: string,
|
|
7473
|
+
record: StatsRecord<TypingIndicatorAbsentTimeoutStats, TypingMeasure>,
|
|
7474
|
+
): void {
|
|
7475
|
+
record.stats.commit();
|
|
7476
|
+
this.cleanupTurn(requestId);
|
|
7477
|
+
}
|
|
7478
|
+
|
|
7479
|
+
private cleanupTurn(requestId: string): void {
|
|
7480
|
+
const record = this.records.get(requestId);
|
|
7481
|
+
if (!record) return;
|
|
7482
|
+
if (record.measure.timer !== null) clearTimeout(record.measure.timer);
|
|
7483
|
+
this.records.delete(requestId);
|
|
6819
7484
|
}
|
|
6820
7485
|
|
|
6821
7486
|
private recordBeforeTpst(
|
|
6822
|
-
|
|
7487
|
+
record: StatsRecord<TypingIndicatorAbsentTimeoutStats, TypingMeasure>,
|
|
6823
7488
|
endedAt: number,
|
|
6824
7489
|
): void {
|
|
6825
|
-
if (
|
|
6826
|
-
const duration = endedAt -
|
|
6827
|
-
if (duration >= TYPING_ABSENT_THRESHOLD_MS)
|
|
6828
|
-
|
|
7490
|
+
if (record.measure.beforeTpstStartedAt === null) return;
|
|
7491
|
+
const duration = endedAt - record.measure.beforeTpstStartedAt;
|
|
7492
|
+
if (duration >= TYPING_ABSENT_THRESHOLD_MS) record.stats.setBeforeTpstAbsentMs(duration);
|
|
7493
|
+
record.measure.beforeTpstStartedAt = null;
|
|
6829
7494
|
}
|
|
6830
7495
|
|
|
6831
7496
|
private recordAfterTpen(
|
|
6832
|
-
|
|
7497
|
+
record: StatsRecord<TypingIndicatorAbsentTimeoutStats, TypingMeasure>,
|
|
6833
7498
|
endedAt: number,
|
|
6834
7499
|
): void {
|
|
6835
|
-
if (
|
|
6836
|
-
const duration = endedAt -
|
|
6837
|
-
if (duration >= TYPING_ABSENT_THRESHOLD_MS)
|
|
6838
|
-
|
|
6839
|
-
}
|
|
6840
|
-
|
|
6841
|
-
private commitIfAbsent(requestId: string, turn: { beforeTpstAbsentMs?: number; afterTpenAbsentMs?: number }): void {
|
|
6842
|
-
const beforeTpstAbsentMs = turn.beforeTpstAbsentMs;
|
|
6843
|
-
const afterTpenAbsentMs = turn.afterTpenAbsentMs;
|
|
6844
|
-
if (beforeTpstAbsentMs === undefined && afterTpenAbsentMs === undefined) {
|
|
6845
|
-
this.cleanupTurn(requestId);
|
|
6846
|
-
return;
|
|
6847
|
-
}
|
|
6848
|
-
|
|
6849
|
-
append(
|
|
6850
|
-
this.callback,
|
|
6851
|
-
METRIC_KEY_AI_TYPING_INDICATOR_ABSENT_TIMEOUT,
|
|
6852
|
-
(beforeTpstAbsentMs ?? 0) + (afterTpenAbsentMs ?? 0),
|
|
6853
|
-
{
|
|
6854
|
-
extra: {
|
|
6855
|
-
...(beforeTpstAbsentMs !== undefined && { before_tpst_absent_ms: beforeTpstAbsentMs }),
|
|
6856
|
-
...(afterTpenAbsentMs !== undefined && { after_tpen_absent_ms: afterTpenAbsentMs }),
|
|
6857
|
-
},
|
|
6858
|
-
},
|
|
6859
|
-
);
|
|
6860
|
-
this.cleanupTurn(requestId);
|
|
6861
|
-
}
|
|
6862
|
-
|
|
6863
|
-
private cleanupTurn(requestId: string): void {
|
|
6864
|
-
const turn = this.turns.get(requestId);
|
|
6865
|
-
if (!turn) return;
|
|
6866
|
-
clearTimer(turn.timer, this.clearTimeoutFn);
|
|
6867
|
-
this.turns.delete(requestId);
|
|
7500
|
+
if (record.measure.afterTpenStartedAt === null) return;
|
|
7501
|
+
const duration = endedAt - record.measure.afterTpenStartedAt;
|
|
7502
|
+
if (duration >= TYPING_ABSENT_THRESHOLD_MS) record.stats.setAfterTpenAbsentMs(duration);
|
|
7503
|
+
record.measure.afterTpenStartedAt = null;
|
|
6868
7504
|
}
|
|
6869
7505
|
}
|
|
6870
7506
|
|
|
6871
|
-
declare type
|
|
6872
|
-
requestId: string;
|
|
6873
|
-
channelUrl?: string;
|
|
6874
|
-
userMessageId?: number;
|
|
6875
|
-
beforeTpstStartedAt: number | null;
|
|
7507
|
+
declare type TypingMeasure = {
|
|
6876
7508
|
afterTpenStartedAt: number | null;
|
|
6877
|
-
|
|
6878
|
-
|
|
7509
|
+
beforeTpstStartedAt: number | null;
|
|
7510
|
+
channelUrl?: string;
|
|
7511
|
+
requestId: string;
|
|
6879
7512
|
timer: TimeoutId | null;
|
|
7513
|
+
userMessageId?: number;
|
|
6880
7514
|
};
|
|
6881
7515
|
|
|
6882
7516
|
declare type TypingTurnEntry = {
|
|
6883
7517
|
requestId: string;
|
|
6884
|
-
|
|
7518
|
+
record: StatsRecord<TypingIndicatorAbsentTimeoutStats, TypingMeasure>;
|
|
6885
7519
|
};
|
|
6886
7520
|
|
|
6887
7521
|
declare interface TypographyShape {
|
|
@@ -6959,76 +7593,163 @@ export declare type UserActionStatus =
|
|
|
6959
7593
|
|
|
6960
7594
|
export declare const useRefreshActiveChannel: (updater: () => Promise<void>) => () => Promise<void>;
|
|
6961
7595
|
|
|
6962
|
-
declare
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
7596
|
+
declare type UserPerceivedResponseTimeMeasure = {
|
|
7597
|
+
ackAt?: number;
|
|
7598
|
+
aiMessageId?: number;
|
|
7599
|
+
channelUrl?: string;
|
|
7600
|
+
firstChunkAt?: number;
|
|
7601
|
+
isStreaming?: boolean;
|
|
7602
|
+
requestId: string;
|
|
7603
|
+
startedAt: number;
|
|
7604
|
+
userMessageId?: number;
|
|
7605
|
+
};
|
|
7606
|
+
|
|
7607
|
+
declare class UserPerceivedResponseTimeStats extends AIAgentBaseStats {
|
|
7608
|
+
private ackToFirstChunkMs?: number;
|
|
7609
|
+
private firstChunkToRenderMs?: number;
|
|
7610
|
+
private isStreaming?: boolean;
|
|
7611
|
+
private sendToAckMs?: number;
|
|
7612
|
+
private totalDurationMs?: number;
|
|
7613
|
+
|
|
7614
|
+
setDurations(params: {
|
|
7615
|
+
ackToFirstChunkMs: number;
|
|
7616
|
+
firstChunkToRenderMs: number;
|
|
7617
|
+
isStreaming: boolean;
|
|
7618
|
+
sendToAckMs: number;
|
|
7619
|
+
totalDurationMs: number;
|
|
7620
|
+
}): this {
|
|
7621
|
+
this.ackToFirstChunkMs = params.ackToFirstChunkMs;
|
|
7622
|
+
this.firstChunkToRenderMs = params.firstChunkToRenderMs;
|
|
7623
|
+
this.isStreaming = params.isStreaming;
|
|
7624
|
+
this.sendToAckMs = params.sendToAckMs;
|
|
7625
|
+
this.totalDurationMs = params.totalDurationMs;
|
|
7626
|
+
return this;
|
|
7627
|
+
}
|
|
7628
|
+
|
|
7629
|
+
protected getMetricKey(): string {
|
|
7630
|
+
return METRIC_KEY_USER_PERCEIVED_RESPONSE_TIME;
|
|
7631
|
+
}
|
|
7632
|
+
|
|
7633
|
+
protected buildPayload(): AIAgentStatPayload | null {
|
|
7634
|
+
if (
|
|
7635
|
+
this.ackToFirstChunkMs === undefined ||
|
|
7636
|
+
this.firstChunkToRenderMs === undefined ||
|
|
7637
|
+
this.isStreaming === undefined ||
|
|
7638
|
+
this.sendToAckMs === undefined ||
|
|
7639
|
+
this.totalDurationMs === undefined
|
|
7640
|
+
) {
|
|
7641
|
+
return null;
|
|
7642
|
+
}
|
|
7643
|
+
|
|
7644
|
+
return this.createPayload(this.totalDurationMs, {
|
|
7645
|
+
extra: {
|
|
7646
|
+
is_streaming: this.isStreaming,
|
|
7647
|
+
[DurationKey.ACK_TO_FIRST_CHUNK]: this.ackToFirstChunkMs,
|
|
7648
|
+
[DurationKey.FIRST_CHUNK_TO_RENDER]: this.firstChunkToRenderMs,
|
|
7649
|
+
[DurationKey.SEND_TO_ACK]: this.sendToAckMs,
|
|
7650
|
+
},
|
|
7651
|
+
});
|
|
7652
|
+
}
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
/**
|
|
7656
|
+
* Spec: user_perceived_response_time
|
|
7657
|
+
*
|
|
7658
|
+
* Measures the user-visible latency from SDK send to the first AI response
|
|
7659
|
+
* frame on screen. Sampling is per user message: 15% independent probability
|
|
7660
|
+
* by default, capped at 10 collected turns per runtime session.
|
|
7661
|
+
*
|
|
7662
|
+
* Timeline:
|
|
7663
|
+
* - T0: SDK send API is called, not the UI tap time.
|
|
7664
|
+
* - T1: send completion handler runs.
|
|
7665
|
+
* - T2: first raw AI response chunk reaches the SDK.
|
|
7666
|
+
* - T3: the first response frame is rendered in the visible message cell.
|
|
7667
|
+
*
|
|
7668
|
+
* Payload:
|
|
7669
|
+
* - value: total_perceived_ms = T3 - T0.
|
|
7670
|
+
* - extra.send_to_ack_ms = T1 - T0.
|
|
7671
|
+
* - extra.ack_to_first_chunk_ms = T2 - T1.
|
|
7672
|
+
* - extra.first_chunk_to_render_ms = T3 - T2.
|
|
7673
|
+
* - extra.is_streaming: whether the AI response is streaming.
|
|
7674
|
+
*
|
|
7675
|
+
* Policy:
|
|
7676
|
+
* - Turn-scoped; commit only after T3.
|
|
7677
|
+
* - Incomplete or aborted turns are not recorded.
|
|
7678
|
+
* - Human-agent handoff/abort clears pending measurements without emitting.
|
|
7679
|
+
*/
|
|
7680
|
+
declare class UserPerceivedResponseTimeStatsTracker extends MultiStatsTrackerBase<
|
|
7681
|
+
UserPerceivedResponseTimeStats,
|
|
7682
|
+
UserPerceivedResponseTimeMeasure
|
|
7683
|
+
> {
|
|
7684
|
+
private readonly samplingRate: number;
|
|
6968
7685
|
private readonly pendingAIResponseByUserMessageId = new Map<number, PendingAIResponseRaw>();
|
|
6969
7686
|
private collectedCount = 0;
|
|
6970
7687
|
|
|
6971
|
-
constructor(callback: StatsAppendCallback, options?:
|
|
6972
|
-
|
|
6973
|
-
this.
|
|
6974
|
-
this.random = options?.random ?? Math.random;
|
|
7688
|
+
constructor(callback: StatsAppendCallback, options?: { samplingRate?: number }) {
|
|
7689
|
+
super(callback);
|
|
7690
|
+
this.samplingRate = options?.samplingRate ?? USER_PERCEIVED_RESPONSE_TIME_SAMPLING_RATE;
|
|
6975
7691
|
}
|
|
6976
7692
|
|
|
6977
7693
|
startTurn(params: { requestId: string; channelUrl?: string }): void {
|
|
6978
|
-
if (this.collectedCount >=
|
|
6979
|
-
if (this.
|
|
7694
|
+
if (this.collectedCount >= MAX_COLLECTED_COUNT) return;
|
|
7695
|
+
if (!isIncludedInSample(this.samplingRate)) return;
|
|
6980
7696
|
this.collectedCount += 1;
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
7697
|
+
|
|
7698
|
+
const record: StatsRecord<UserPerceivedResponseTimeStats, UserPerceivedResponseTimeMeasure> = {
|
|
7699
|
+
stats: new UserPerceivedResponseTimeStats().setCommitCallback(this.commitCallback),
|
|
7700
|
+
measure: {
|
|
7701
|
+
channelUrl: params.channelUrl,
|
|
7702
|
+
requestId: params.requestId,
|
|
7703
|
+
startedAt: performance.now(),
|
|
7704
|
+
},
|
|
7705
|
+
};
|
|
7706
|
+
this.records.set(params.requestId, record);
|
|
6986
7707
|
}
|
|
6987
7708
|
|
|
6988
7709
|
markUserMessageAck(params: { requestId: string; messageId: number }): void {
|
|
6989
|
-
const
|
|
6990
|
-
if (!
|
|
7710
|
+
const record = this.records.get(params.requestId);
|
|
7711
|
+
if (!record) return;
|
|
6991
7712
|
|
|
6992
|
-
|
|
6993
|
-
|
|
7713
|
+
record.measure.ackAt = performance.now();
|
|
7714
|
+
record.measure.userMessageId = params.messageId;
|
|
6994
7715
|
|
|
6995
7716
|
const pending = this.pendingAIResponseByUserMessageId.get(params.messageId);
|
|
6996
7717
|
if (!pending) return;
|
|
6997
7718
|
this.pendingAIResponseByUserMessageId.delete(params.messageId);
|
|
6998
|
-
this.applyAIResponseRaw(
|
|
7719
|
+
this.applyAIResponseRaw(record, pending);
|
|
6999
7720
|
}
|
|
7000
7721
|
|
|
7001
7722
|
replaceTurnRequestId(currentRequestId: string, nextRequestId: string): void {
|
|
7002
7723
|
if (currentRequestId === nextRequestId) return;
|
|
7003
7724
|
|
|
7004
|
-
const
|
|
7005
|
-
if (!
|
|
7725
|
+
const record = this.records.get(currentRequestId);
|
|
7726
|
+
if (!record) return;
|
|
7006
7727
|
|
|
7007
|
-
this.
|
|
7008
|
-
|
|
7009
|
-
this.
|
|
7728
|
+
this.records.delete(currentRequestId);
|
|
7729
|
+
record.measure.requestId = nextRequestId;
|
|
7730
|
+
this.records.set(nextRequestId, record);
|
|
7010
7731
|
}
|
|
7011
7732
|
|
|
7012
7733
|
cancelTurn(requestId: string): void {
|
|
7013
|
-
const
|
|
7014
|
-
if (!
|
|
7015
|
-
this.
|
|
7734
|
+
const record = this.records.get(requestId);
|
|
7735
|
+
if (!record) return;
|
|
7736
|
+
this.deleteRecord(record);
|
|
7016
7737
|
// Return the unused sample slot so cancelled turns don't burn the per-session cap.
|
|
7017
7738
|
if (this.collectedCount > 0) this.collectedCount -= 1;
|
|
7018
7739
|
}
|
|
7019
7740
|
|
|
7020
7741
|
markAIResponseRaw(params: { messageId: number; channelUrl?: string; aiResponse: AIResponseMessageMetadata }): void {
|
|
7021
|
-
const firstChunkAt =
|
|
7742
|
+
const firstChunkAt = performance.now();
|
|
7022
7743
|
const userMessageId = params.aiResponse.userMessageId;
|
|
7023
|
-
if (userMessageId === undefined && this.
|
|
7744
|
+
if (userMessageId === undefined && hasUnackedChannelTurn(this.records.values(), params.channelUrl)) return;
|
|
7024
7745
|
|
|
7025
|
-
const
|
|
7026
|
-
if (
|
|
7027
|
-
this.applyAIResponseRaw(
|
|
7746
|
+
const record = this.findPendingRecord(params.channelUrl, userMessageId);
|
|
7747
|
+
if (record) {
|
|
7748
|
+
this.applyAIResponseRaw(record, { ...params, firstChunkAt });
|
|
7028
7749
|
return;
|
|
7029
7750
|
}
|
|
7030
7751
|
|
|
7031
|
-
if (userMessageId === undefined || !this.
|
|
7752
|
+
if (userMessageId === undefined || !hasUnackedChannelTurn(this.records.values(), params.channelUrl)) return;
|
|
7032
7753
|
this.bufferAIResponseRaw(
|
|
7033
7754
|
{
|
|
7034
7755
|
messageId: params.messageId,
|
|
@@ -7041,26 +7762,31 @@ declare class UserPerceivedResponseTimeStatsTracker {
|
|
|
7041
7762
|
}
|
|
7042
7763
|
|
|
7043
7764
|
markAIResponseVisible(aiMessageId: number): boolean {
|
|
7044
|
-
const
|
|
7045
|
-
if (!
|
|
7046
|
-
return false;
|
|
7765
|
+
const record = this.findRecordByAIMessageId(aiMessageId);
|
|
7766
|
+
if (!record) return false;
|
|
7047
7767
|
|
|
7048
|
-
const
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
first_chunk_to_render_ms: renderedAt - turn.firstChunkAt,
|
|
7053
|
-
is_streaming: Boolean(turn.isStreaming),
|
|
7054
|
-
send_to_ack_ms: turn.ackAt - turn.startedAt,
|
|
7055
|
-
},
|
|
7056
|
-
});
|
|
7768
|
+
const measure = record.measure;
|
|
7769
|
+
if (measure.ackAt === undefined || measure.firstChunkAt === undefined || measure.userMessageId === undefined) {
|
|
7770
|
+
return false;
|
|
7771
|
+
}
|
|
7057
7772
|
|
|
7058
|
-
|
|
7773
|
+
const renderedAt = performance.now();
|
|
7774
|
+
record.stats
|
|
7775
|
+
.setDurations({
|
|
7776
|
+
ackToFirstChunkMs: measure.firstChunkAt - measure.ackAt,
|
|
7777
|
+
firstChunkToRenderMs: renderedAt - measure.firstChunkAt,
|
|
7778
|
+
isStreaming: Boolean(measure.isStreaming),
|
|
7779
|
+
sendToAckMs: measure.ackAt - measure.startedAt,
|
|
7780
|
+
totalDurationMs: renderedAt - measure.startedAt,
|
|
7781
|
+
})
|
|
7782
|
+
.commit();
|
|
7783
|
+
|
|
7784
|
+
this.deleteRecord(record);
|
|
7059
7785
|
return true;
|
|
7060
7786
|
}
|
|
7061
7787
|
|
|
7062
7788
|
hasPendingAIResponse(aiMessageId: number): boolean {
|
|
7063
|
-
return this.
|
|
7789
|
+
return this.findRecordByAIMessageId(aiMessageId) !== undefined;
|
|
7064
7790
|
}
|
|
7065
7791
|
|
|
7066
7792
|
clearMessage(aiMessageId: number): void {
|
|
@@ -7068,18 +7794,18 @@ declare class UserPerceivedResponseTimeStatsTracker {
|
|
|
7068
7794
|
if (pending.messageId === aiMessageId) this.pendingAIResponseByUserMessageId.delete(userMessageId);
|
|
7069
7795
|
});
|
|
7070
7796
|
|
|
7071
|
-
const
|
|
7072
|
-
if (!
|
|
7797
|
+
const record = this.findRecordByAIMessageId(aiMessageId);
|
|
7798
|
+
if (!record) return;
|
|
7073
7799
|
|
|
7074
|
-
this.
|
|
7800
|
+
this.deleteRecord(record);
|
|
7075
7801
|
if (this.collectedCount > 0) this.collectedCount -= 1;
|
|
7076
7802
|
}
|
|
7077
7803
|
|
|
7078
7804
|
clearChannel(channelUrl?: string): void {
|
|
7079
7805
|
if (!channelUrl) return;
|
|
7080
|
-
this.
|
|
7081
|
-
if (
|
|
7082
|
-
this.
|
|
7806
|
+
this.records.forEach((record) => {
|
|
7807
|
+
if (record.measure.channelUrl !== channelUrl) return;
|
|
7808
|
+
this.deleteRecord(record);
|
|
7083
7809
|
if (this.collectedCount > 0) this.collectedCount -= 1;
|
|
7084
7810
|
});
|
|
7085
7811
|
this.pendingAIResponseByUserMessageId.forEach((pending, userMessageId) => {
|
|
@@ -7088,40 +7814,54 @@ declare class UserPerceivedResponseTimeStatsTracker {
|
|
|
7088
7814
|
}
|
|
7089
7815
|
|
|
7090
7816
|
clear(): void {
|
|
7091
|
-
this.
|
|
7092
|
-
this.sampledByAIMessageId.clear();
|
|
7817
|
+
this.records.clear();
|
|
7093
7818
|
this.pendingAIResponseByUserMessageId.clear();
|
|
7094
7819
|
this.collectedCount = 0;
|
|
7095
7820
|
}
|
|
7096
7821
|
|
|
7097
|
-
private
|
|
7822
|
+
private findRecordByAIMessageId(
|
|
7823
|
+
aiMessageId: number,
|
|
7824
|
+
): StatsRecord<UserPerceivedResponseTimeStats, UserPerceivedResponseTimeMeasure> | undefined {
|
|
7825
|
+
for (const record of this.records.values()) {
|
|
7826
|
+
if (record.measure.aiMessageId === aiMessageId) return record;
|
|
7827
|
+
}
|
|
7828
|
+
return undefined;
|
|
7829
|
+
}
|
|
7830
|
+
|
|
7831
|
+
private findPendingRecord(
|
|
7832
|
+
channelUrl?: string,
|
|
7833
|
+
userMessageId?: number,
|
|
7834
|
+
): StatsRecord<UserPerceivedResponseTimeStats, UserPerceivedResponseTimeMeasure> | undefined {
|
|
7098
7835
|
if (userMessageId !== undefined) {
|
|
7099
|
-
for (const
|
|
7100
|
-
if (
|
|
7101
|
-
if (!matchesChannel(
|
|
7102
|
-
if (
|
|
7836
|
+
for (const record of this.records.values()) {
|
|
7837
|
+
if (record.measure.ackAt === undefined || record.measure.firstChunkAt !== undefined) continue;
|
|
7838
|
+
if (!matchesChannel(record.measure.channelUrl, channelUrl)) continue;
|
|
7839
|
+
if (record.measure.userMessageId === userMessageId) return record;
|
|
7103
7840
|
}
|
|
7104
7841
|
return undefined;
|
|
7105
7842
|
}
|
|
7106
7843
|
|
|
7107
|
-
let match:
|
|
7108
|
-
for (const
|
|
7109
|
-
if (
|
|
7110
|
-
if (!matchesChannel(
|
|
7844
|
+
let match: StatsRecord<UserPerceivedResponseTimeStats, UserPerceivedResponseTimeMeasure> | undefined;
|
|
7845
|
+
for (const record of this.records.values()) {
|
|
7846
|
+
if (record.measure.ackAt === undefined || record.measure.firstChunkAt !== undefined) continue;
|
|
7847
|
+
if (!matchesChannel(record.measure.channelUrl, channelUrl)) continue;
|
|
7111
7848
|
if (match) return undefined;
|
|
7112
|
-
match =
|
|
7849
|
+
match = record;
|
|
7113
7850
|
}
|
|
7114
7851
|
return match;
|
|
7115
7852
|
}
|
|
7116
7853
|
|
|
7117
|
-
private applyAIResponseRaw(
|
|
7118
|
-
|
|
7854
|
+
private applyAIResponseRaw(
|
|
7855
|
+
record: StatsRecord<UserPerceivedResponseTimeStats, UserPerceivedResponseTimeMeasure>,
|
|
7856
|
+
params: PendingAIResponseRaw,
|
|
7857
|
+
): void {
|
|
7858
|
+
if (record.measure.firstChunkAt !== undefined) return;
|
|
7119
7859
|
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7860
|
+
record.measure.firstChunkAt =
|
|
7861
|
+
record.measure.ackAt === undefined ? params.firstChunkAt : Math.max(params.firstChunkAt, record.measure.ackAt);
|
|
7862
|
+
record.measure.aiMessageId = params.messageId;
|
|
7863
|
+
record.measure.channelUrl = record.measure.channelUrl ?? params.channelUrl;
|
|
7864
|
+
record.measure.isStreaming = params.aiResponse.isStreaming;
|
|
7125
7865
|
}
|
|
7126
7866
|
|
|
7127
7867
|
private bufferAIResponseRaw(params: PendingAIResponseRaw, userMessageId: number): void {
|
|
@@ -7131,14 +7871,10 @@ declare class UserPerceivedResponseTimeStatsTracker {
|
|
|
7131
7871
|
}
|
|
7132
7872
|
}
|
|
7133
7873
|
|
|
7134
|
-
private
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
private deleteTurn(turn: UserTurn): void {
|
|
7139
|
-
this.sampledByRequestId.delete(turn.requestId);
|
|
7140
|
-
if (turn.aiMessageId !== undefined) this.sampledByAIMessageId.delete(turn.aiMessageId);
|
|
7141
|
-
if (turn.userMessageId !== undefined) this.pendingAIResponseByUserMessageId.delete(turn.userMessageId);
|
|
7874
|
+
private deleteRecord(record: StatsRecord<UserPerceivedResponseTimeStats, UserPerceivedResponseTimeMeasure>): void {
|
|
7875
|
+
this.records.delete(record.measure.requestId);
|
|
7876
|
+
if (record.measure.userMessageId !== undefined)
|
|
7877
|
+
this.pendingAIResponseByUserMessageId.delete(record.measure.userMessageId);
|
|
7142
7878
|
}
|
|
7143
7879
|
}
|
|
7144
7880
|
|
|
@@ -7159,17 +7895,6 @@ declare interface UserSessionInfo {
|
|
|
7159
7895
|
sessionHandler: AIAgentSessionHandler;
|
|
7160
7896
|
}
|
|
7161
7897
|
|
|
7162
|
-
declare type UserTurn = {
|
|
7163
|
-
requestId: string;
|
|
7164
|
-
channelUrl?: string;
|
|
7165
|
-
startedAt: number;
|
|
7166
|
-
ackAt?: number;
|
|
7167
|
-
firstChunkAt?: number;
|
|
7168
|
-
userMessageId?: number;
|
|
7169
|
-
aiMessageId?: number;
|
|
7170
|
-
isStreaming?: boolean;
|
|
7171
|
-
};
|
|
7172
|
-
|
|
7173
7898
|
export { }
|
|
7174
7899
|
|
|
7175
7900
|
|