From 30dcfd447bdb995ba2b41cb3978a7d86656ad9f7 Mon Sep 17 00:00:00 2001 From: dor-eitan <164745144+dor-eitan@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:30:03 +0200 Subject: [PATCH] bugfix: defer analytics (#242) * bugfix: defer analytics * fix test --- src/services/agent-manager/index.test.ts | 11 +++++++++-- src/services/agent-manager/index.ts | 13 +++++++++++-- src/services/analytics/mixpanel.ts | 19 +++++++++++-------- src/utils/defer.ts | 13 +++++++++++++ 4 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 src/utils/defer.ts diff --git a/src/services/agent-manager/index.test.ts b/src/services/agent-manager/index.test.ts index 075bdaf0..430e0f28 100644 --- a/src/services/agent-manager/index.test.ts +++ b/src/services/agent-manager/index.test.ts @@ -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) }, @@ -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 () => { diff --git a/src/services/agent-manager/index.ts b/src/services/agent-manager/index.ts index cf3068b1..c4c2090c 100644 --- a/src/services/agent-manager/index.ts +++ b/src/services/agent-manager/index.ts @@ -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'; @@ -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); @@ -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); diff --git a/src/services/analytics/mixpanel.ts b/src/services/analytics/mixpanel.ts index 76dd775b..3c24032f 100644 --- a/src/services/analytics/mixpanel.ts +++ b/src/services/analytics/mixpanel.ts @@ -17,7 +17,7 @@ export interface Analytics { agentId: string; owner_id?: string; getRandom(): string; - track(event: string, props?: Record): Promise; + track(event: string, props?: Record, eventTimestamp?: number): Promise; linkTrack(mixpanelEvent: string, props: Record, event: string, dependencies: string[]): any; enrich(props: Record): void; additionalProperties: Record; @@ -53,7 +53,7 @@ export function initializeAnalytics(config: AnalyticsOptions): Analytics { enrich(props: Record) { this.additionalProperties = { ...this.additionalProperties, ...props }; }, - async track(event: string, props?: Record) { + async track(event: string, props?: Record, eventTimestamp?: number) { if (!this.isEnabled) { return Promise.resolve(); } @@ -61,6 +61,8 @@ export function initializeAnalytics(config: AnalyticsOptions): Analytics { // Ignore audioPath event from agent-video const { audioPath, ...sendProps } = props || {}; + const eventTime = eventTimestamp || Date.now(); + const options = { method: 'POST', headers: { @@ -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, @@ -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, event: string, dependencies: string[]) { if (!mixpanelEvents[mixpanelEvent]) { diff --git a/src/utils/defer.ts b/src/utils/defer.ts new file mode 100644 index 00000000..9bf2fb63 --- /dev/null +++ b/src/utils/defer.ts @@ -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); + } +}