import { useCallback, useEffect, useMemo, useRef } from 'react' import { useChatContext } from '../contexts/chat_context' import { ProductAnalyticsConfig } from '../types/product_analytics' import { AnalyticsMetadataResource } from '../types/resources/analytics_metadata' import { keysToSnakeCase } from '../utils/client/utils' import { SHA256, enc } from '../utils/sha_256' export interface EventMetadata { [key: string]: string | number | boolean | null | undefined } export const analyticsEvents = { conversation_index_opened: 'chat.mobile.conversations.index.opened', conversation_show_opened: 'chat.mobile.conversations.show.opened', search_performed: 'chat.mobile.search.performed', search_result_opened: 'chat.mobile.search.result.opened', } as const satisfies Record type AnalyticsEventName = (typeof analyticsEvents)[keyof typeof analyticsEvents] interface AnalyticsEvent { name: string meta: EventMetadata } function contentSha256(payload: string): string { return SHA256(payload).toString(enc.Hex) } function sendEvents(events: AnalyticsEvent[], config: ProductAnalyticsConfig): Promise { // Split into chunks of 10 and make parallel requests const chunks: AnalyticsEvent[][] = [] for (let i = 0; i < events.length; i += 10) { chunks.push(events.slice(i, i + 10)) } return Promise.all( chunks.map(async chunk => { const payload = JSON.stringify({ events: chunk }) return fetch(config.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', 'X-Authorization': `Bearer ${config.token}`, 'X-Amz-Content-Sha256': contentSha256(payload), }, body: payload, }) }) ).catch(error => { if (__DEV__) { console.warn('Product Analytics request failed:', error) } return [] as Response[] }) } export const useProductAnalytics = () => { const { productAnalyticsConfig } = useChatContext() const snakeCasedBaseMeta = useMemo( () => keysToSnakeCase(productAnalyticsConfig?.metadata || {}), [productAnalyticsConfig?.metadata] ) const publishEvent = useCallback( (name: AnalyticsEventName, meta: EventMetadata = {}): Promise => { if (!productAnalyticsConfig) { if (__DEV__) { console.warn('Product Analytics not available') } return Promise.resolve([]) } const mergedMeta = { ...snakeCasedBaseMeta, ...meta } as EventMetadata return sendEvents([{ name, meta: mergedMeta }], productAnalyticsConfig) }, [productAnalyticsConfig, snakeCasedBaseMeta] ) const publishEvents = useCallback( (events: Array<{ name: AnalyticsEventName; meta?: EventMetadata }>): Promise => { if (!productAnalyticsConfig) { if (__DEV__) { console.warn('Product Analytics not available') } return Promise.resolve([]) } const eventsWithMeta = events.map(event => ({ name: event.name, meta: { ...snakeCasedBaseMeta, ...event.meta } as EventMetadata, })) return sendEvents(eventsWithMeta, productAnalyticsConfig) }, [productAnalyticsConfig, snakeCasedBaseMeta] ) return { publishEvent, publishEvents } } export function usePublishProductAnalyticsEvent( eventName: AnalyticsEventName, meta: EventMetadata = {} ) { const { publishEvent } = useProductAnalytics() const hasPublishedEventRef = useRef(false) useEffect(() => { if (hasPublishedEventRef.current) return hasPublishedEventRef.current = true publishEvent(eventName, meta) }, [eventName, meta, publishEvent]) } export function normalizeAnalyticsMetadata(resource: { analyticsMetadata?: AnalyticsMetadataResource }) { return keysToSnakeCase(resource?.analyticsMetadata?.metadata || {}) as Record }