Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/services/agent-manager/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jest.mock('../../utils/analytics', () => ({
getAgentInfo: jest.fn(() => ({ agentType: 'talk' })),
getAnalyticsInfo: jest.fn(() => ({ agentType: 'talk' })),
}));
jest.mock('../../utils/defer', () => ({
defer: jest.fn(fn => fn()),
}));
jest.mock('../analytics/timestamp-tracker', () => ({
latencyTimestampTracker: { reset: jest.fn(), update: jest.fn(() => Date.now()), get: jest.fn(() => 1000) },
interruptTimestampTracker: { reset: jest.fn(), update: jest.fn(), get: jest.fn(() => 500) },
Expand Down Expand Up @@ -108,8 +111,12 @@ describe('createAgentManager', () => {
isEnabled: true,
externalId: undefined,
});
expect(mockAnalytics.track).toHaveBeenCalledWith('agent-sdk', { event: 'init' });
expect(mockAnalytics.track).toHaveBeenCalledWith('agent-sdk', expect.objectContaining({ event: 'loaded' }));
expect(mockAnalytics.track).toHaveBeenCalledWith('agent-sdk', { event: 'init' }, expect.any(Number));
expect(mockAnalytics.track).toHaveBeenCalledWith(
'agent-sdk',
expect.objectContaining({ event: 'loaded' }),
expect.any(Number)
);
});

it('should use custom configuration options', async () => {
Expand Down
13 changes: 11 additions & 2 deletions src/services/agent-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { isStreamsV2Agent } from '@sdk/utils/agent';
import { isChatModeWithoutChat, isTextualChat } from '@sdk/utils/chat';
import { createAgentsApi } from '../../api/agents';
import { getAgentInfo, getAnalyticsInfo } from '../../utils/analytics';
import { defer } from '../../utils/defer';
import { retryOperation } from '../../utils/retry-operation';
import { initializeAnalytics } from '../analytics/mixpanel';
import { interruptTimestampTracker, latencyTimestampTracker } from '../analytics/timestamp-tracker';
Expand Down Expand Up @@ -70,7 +71,12 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt
externalId: options.externalId,
mixpanelAdditionalProperties: options.mixpanelAdditionalProperties,
});
analytics.track('agent-sdk', { event: 'init' });

const initTimestamp = Date.now();
defer(() => {
analytics.track('agent-sdk', { event: 'init' }, initTimestamp);
});

const agentsApi = createAgentsApi(options.auth, baseURL, options.callbacks.onError, options.externalId);

const agentEntity = await agentsApi.getById(agent);
Expand All @@ -89,7 +95,10 @@ export async function createAgentManager(agent: string, options: AgentManagerOpt
videoId = newVideoId;
};

analytics.track('agent-sdk', { event: 'loaded', ...getAnalyticsInfo(agentEntity) });
const loadedTimestamp = Date.now();
defer(() => {
analytics.track('agent-sdk', { event: 'loaded', ...getAnalyticsInfo(agentEntity) }, loadedTimestamp);
});

async function connect(newChat: boolean) {
options.callbacks.onConnectionStateChange?.(ConnectionState.Connecting);
Expand Down
19 changes: 11 additions & 8 deletions src/services/analytics/mixpanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface Analytics {
agentId: string;
owner_id?: string;
getRandom(): string;
track(event: string, props?: Record<string, any>): Promise<any>;
track(event: string, props?: Record<string, any>, eventTimestamp?: number): Promise<any>;
linkTrack(mixpanelEvent: string, props: Record<string, any>, event: string, dependencies: string[]): any;
enrich(props: Record<string, any>): void;
additionalProperties: Record<string, any>;
Expand Down Expand Up @@ -53,14 +53,16 @@ export function initializeAnalytics(config: AnalyticsOptions): Analytics {
enrich(props: Record<string, any>) {
this.additionalProperties = { ...this.additionalProperties, ...props };
},
async track(event: string, props?: Record<string, any>) {
async track(event: string, props?: Record<string, any>, eventTimestamp?: number) {
if (!this.isEnabled) {
return Promise.resolve();
}

// Ignore audioPath event from agent-video
const { audioPath, ...sendProps } = props || {};

const eventTime = eventTimestamp || Date.now();

const options = {
method: 'POST',
headers: {
Expand All @@ -76,7 +78,7 @@ export function initializeAnalytics(config: AnalyticsOptions): Analytics {
agentId: this.agentId,
source,
token: this.token,
time: Date.now(),
time: eventTime,
$insert_id: this.getRandom(),
origin: window.location.href,
'Screen Height': window.screen.height || window.innerWidth,
Expand All @@ -88,11 +90,12 @@ export function initializeAnalytics(config: AnalyticsOptions): Analytics {
}),
};

try {
return await fetch(mixpanelUrl, options).then(res => res.json());
} catch (err) {
return console.error(err);
}
fetch(mixpanelUrl, {
...options,
keepalive: true,
}).catch(err => console.error('Analytics tracking error:', err));

return Promise.resolve();
},
linkTrack(mixpanelEvent: string, props: Record<string, any>, event: string, dependencies: string[]) {
if (!mixpanelEvents[mixpanelEvent]) {
Expand Down
13 changes: 13 additions & 0 deletions src/utils/defer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Defers function execution until browser is idle to avoid blocking critical path.
* Uses requestIdleCallback when available, falls back to setTimeout.
*
* @param fn - Function to execute when browser is idle
*/
export function defer(fn: () => void) {
if ('requestIdleCallback' in window) {
requestIdleCallback(fn, { timeout: 2000 });
} else {
setTimeout(fn, 0);
}
}