import { hasAnalyticsConsent, isAnalyticsEnvironment } from './analyticsConsent'; import type { Account } from '../data'; type QueuedCall = | { type: 'track'; eventName: string; data?: Record } | { type: 'identify'; userInfo: Record }; const queue: QueuedCall[] = []; /** * Named product events for Tianji (consent-gated). Page views stay on tracker.js. * * | Event | When | * |-------|------| * | Login Success / Failed | Password or SSO sign-in | * | Sign Up Started / Success / Failed | Self-serve registration | * | Logout | Sign out | * | Checkout Started | Stripe Checkout session created | * | Payment Success / Canceled | Billing return URLs | * | Conversation Created | New chat id assigned over WS | * | Message Sent | User submits prompt (no content) | * | ToS Acknowledged | POST acknowledge_tos succeeds | * | Billing Portal Opened | Account manage / portal CTAs | * | Subscription Upgrade Started | Upgrade intent (#75) | * | Plan Change Started | Change-plan intent (#75) | * | Subscription Cancel Started | Cancel intent (#75) | * | Account Delete Started / Success / Failed | Self-delete (#34 companion) | */ export const AnalyticsEvents = { LOGIN_SUCCESS: 'Login Success', LOGIN_FAILED: 'Login Failed', SIGNUP_STARTED: 'Sign Up Started', SIGNUP_SUCCESS: 'Sign Up Success', SIGNUP_FAILED: 'Sign Up Failed', LOGOUT: 'Logout', CHECKOUT_STARTED: 'Checkout Started', PAYMENT_SUCCESS: 'Payment Success', PAYMENT_CANCELED: 'Payment Canceled', CONVERSATION_CREATED: 'Conversation Created', MESSAGE_SENT: 'Message Sent', TOS_ACKNOWLEDGED: 'ToS Acknowledged', BILLING_PORTAL_OPENED: 'Billing Portal Opened', SUBSCRIPTION_UPGRADE_STARTED: 'Subscription Upgrade Started', PLAN_CHANGE_STARTED: 'Plan Change Started', SUBSCRIPTION_CANCEL_STARTED: 'Subscription Cancel Started', ACCOUNT_DELETE_STARTED: 'Account Delete Started', ACCOUNT_DELETE_SUCCESS: 'Account Delete Success', ACCOUNT_DELETE_FAILED: 'Account Delete Failed', } as const; export type AnalyticsEventName = (typeof AnalyticsEvents)[keyof typeof AnalyticsEvents]; /** Custom events + identify require consent. Automatic page views do not. */ const isEventTrackingEnabled = (): boolean => isAnalyticsEnvironment() && hasAnalyticsConsent(); const runQueuedCall = (call: QueuedCall): void => { if (!window.tianji) return; if (call.type === 'track') { window.tianji.track(call.eventName, call.data); return; } window.tianji.identify(call.userInfo); }; export const flushAnalyticsQueue = (): void => { if (!isEventTrackingEnabled() || !window.tianji) return; while (queue.length > 0) { const call = queue.shift(); if (call) runQueuedCall(call); } }; export const trackEvent = ( eventName: AnalyticsEventName | string, data?: Record, ): void => { if (!isEventTrackingEnabled()) return; const payload = data ? { ...data } : undefined; if (window.tianji?.track) { window.tianji.track(eventName, payload); return; } queue.push({ type: 'track', eventName, data: payload }); }; export const identifyUser = (userInfo: Record): void => { if (!isEventTrackingEnabled()) return; if (window.tianji?.identify) { window.tianji.identify(userInfo); return; } queue.push({ type: 'identify', userInfo: { ...userInfo } }); }; /** Identify with stable non-sensitive traits only (no names / prompts). */ export const identifyAccount = (account: Account): void => { const traits: Record = { userId: account.email, }; if (account.company?.id != null) { traits.companyId = account.company.id; } if (account.is_company_manager) { traits.isCompanyManager = true; } identifyUser(traits); };