UNPKG

posthog-js

Version:

Posthog-js allows you to automatically capture usage and send events to PostHog.

532 lines (526 loc) • 15.5 kB
import { PostHog } from './module'; import { ConversationsRemoteConfig } from '@posthog/browser-common'; /** * Author types for messages in a conversation */ type MessageAuthorType = 'customer' | 'AI' | 'human'; /** * TipTap mark types for inline formatting */ interface TipTapMark { type: 'bold' | 'italic' | 'underline' | 'strike' | 'code' | 'link'; attrs?: { href?: string; target?: string; [key: string]: unknown; }; } /** * TipTap node representing content in the document tree */ interface TipTapNode { type: string; attrs?: Record<string, unknown>; content?: TipTapNode[]; marks?: TipTapMark[]; text?: string; } /** * TipTap document - the root node of rich content */ interface TipTapDoc { type: 'doc'; content?: TipTapNode[]; } /** * A message in a conversation */ interface Message { /** * Unique identifier for the message */ id: string; /** * The message content as plain text (fallback) */ content: string; /** * Rich content in TipTap JSON format (preferred for rendering) * Falls back to `content` if missing or invalid */ rich_content?: TipTapDoc; /** * Type of the message author */ author_type: MessageAuthorType; /** * Display name of the message author */ author_name?: string; /** * ISO timestamp when the message was created */ created_at: string; /** * Whether this is an internal note (not shown to customer) */ is_private: boolean; } /** * Status of a support ticket */ type TicketStatus = 'new' | 'open' | 'pending' | 'on_hold' | 'resolved'; /** * A support ticket in the conversations system */ interface Ticket { /** * Unique identifier for the ticket */ id: string; /** * Current status of the ticket */ status: TicketStatus; /** * Preview of the last message */ last_message?: string; /** * ISO timestamp of the last message */ last_message_at?: string; /** * Total number of messages in this ticket */ message_count: number; /** * ISO timestamp when the ticket was created */ created_at: string; /** * Array of messages (only present in detailed ticket view) */ messages?: Message[]; /** * Number of unread messages from the team */ unread_count?: number; } /** * Response from sending a message */ interface SendMessageResponse { /** * ID of the ticket this message belongs to */ ticket_id: string; /** * ID of the newly created message */ message_id: string; /** * Current status of the ticket */ ticket_status: TicketStatus; /** * ISO timestamp when the message was created */ created_at: string; /** * Number of unread messages from the team * After customer sends a message, this is always 0 */ unread_count: number; } /** * Response from fetching messages */ interface GetMessagesResponse { /** * ID of the ticket */ ticket_id: string; /** * Current status of the ticket */ ticket_status: TicketStatus; /** * Array of messages */ messages: Message[]; /** * Whether there are more messages to fetch */ has_more: boolean; /** * Number of unread messages from the team */ unread_count: number; } /** * Response from marking messages as read */ interface MarkAsReadResponse { /** * Whether the operation was successful */ success: boolean; /** * Number of unread messages (should be 0 after marking as read) */ unread_count: number; } /** * Options for fetching tickets list */ interface GetTicketsOptions { /** * Filter by ticket status (e.g., 'open', 'closed') */ status?: string; /** * Number of tickets to return (default: 20) */ limit?: number; /** * Pagination offset (default: 0) */ offset?: number; } /** * Response from fetching tickets list */ interface GetTicketsResponse { /** * Total count of tickets */ count: number; /** * Array of tickets */ results: Ticket[]; } type RestoreFromTokenStatus = 'success' | 'expired' | 'invalid' | 'used'; /** * Response from restore token exchange endpoint */ interface RestoreFromTokenResponse { /** * Restore result */ status: RestoreFromTokenStatus; /** * Canonical widget session ID to use after restore */ widget_session_id?: string; /** * Migrated ticket IDs, if any */ migrated_ticket_ids?: string[]; /** * Optional machine-readable backend code */ code?: string; } /** * Response from self-service restore link request */ interface RequestRestoreLinkResponse { ok: true; } /** * User-provided identification data (collected via the widget form) */ interface UserProvidedTraits { name?: string; email?: string; } interface LazyLoadedConversationsInterface { show: () => void; hide: () => void; isVisible: () => boolean; reset: () => void; setIdentity: () => void; clearIdentity: () => void; sendMessage: (message: string, userTraits?: UserProvidedTraits, newTicket?: boolean) => Promise<SendMessageResponse>; getMessages: (ticketId?: string, after?: string) => Promise<GetMessagesResponse>; markAsRead: (ticketId?: string) => Promise<MarkAsReadResponse>; getTickets: (options?: GetTicketsOptions) => Promise<GetTicketsResponse>; requestRestoreLink: (email: string) => Promise<RequestRestoreLinkResponse>; restoreFromToken: (restoreToken: string) => Promise<RestoreFromTokenResponse>; restoreFromUrlToken: () => Promise<RestoreFromTokenResponse | null>; getCurrentTicketId: () => string | null; getWidgetSessionId: () => string; } type ConversationsManager$1 = LazyLoadedConversationsInterface; declare class ConversationsManager implements ConversationsManager$1 { private readonly _posthog; private _config; private _persistence; private _widgetRef; private _containerElement; private _currentTicketId; private _pollTimeoutId; private _pollLoopRunning; private _pollGeneration; private _consecutivePollingRateLimitFailures; private _reattachIntervalId; private _lastMessageTimestamp; private _isPollingMessages; private _isPollingTickets; private _unsubscribeIdentifyListener; private _unreadCount; private _widgetSessionId; private _isWidgetEnabled; private _isDomainAllowed; private _widgetState; private _isWidgetRendered; private _hasProcessedRestoreToken; private _initializeWidgetPromise; private _currentView; private _tickets; private _showTicketList; private _consecutivePollingStatusZeroFailures; constructor(config: ConversationsRemoteConfig, _posthog: PostHog); private _onOnline; private _currentUrl; /** * Send a message programmatically via the API * Creates a new ticket if none exists or if newTicket is true * * @param message - The message text to send * @param userTraits - Optional user identification data (name, email) * @param newTicket - If true, forces creation of a new ticket (ignores current ticket) * @returns Promise with the response including ticket_id and message_id */ sendMessage(message: string, userTraits?: UserProvidedTraits, newTicket?: boolean): Promise<SendMessageResponse>; /** * Switch to a different ticket if an explicit ticketId is provided * This ensures subsequent operations (sendMessage, etc.) use the correct ticket */ private _switchToTicketIfNeeded; /** Fetch messages via the API */ getMessages(ticketId?: string, after?: string): Promise<GetMessagesResponse>; /** Mark messages as read via the API */ markAsRead(ticketId?: string): Promise<MarkAsReadResponse>; /** * Initialize the conversations manager. * Always initializes persistence and event listeners for API usage. * Only renders the widget if widgetEnabled is true AND domain is allowed. */ private _initialize; private _completeInitialization; private _restoreFromTokenWithRetry; private _restoreFromToken; /** * Initialize and render the widget UI * Uses a promise guard to prevent race conditions from concurrent calls */ private _initializeWidget; private _doInitializeWidget; /** * Extract name and email from PostHog's stored person properties. * * Person properties set via posthog.identify() are stored under the * $stored_person_properties persistence key, not as top-level props. * We check both locations plus the super-properties for completeness. */ private _getPersonTraits; /** * Get initial user traits from PostHog or localStorage */ private _getInitialUserTraits; /** * Handle user identification from the widget form */ private _handleIdentify; private _handleRequestRestoreLink; /** * Handle sending a message from the widget */ private _handleSendMessage; /** * Handle widget state changes */ private _handleStateChange; /** * Mark messages as read */ private _markMessagesAsRead; /** * Load messages for the current ticket */ private _loadMessages; private _isWidgetOpen; /** * Poll for new messages */ private _pollMessages; /** * Poll for tickets list */ private _pollTickets; /** * Load tickets list from API */ private _loadTickets; private _computeShowTicketList; private _isCurrentTicketResolved; /** * Patch the local _tickets cache with a new status for a given ticket and push * any UI-relevant changes (resolved lock + list visibility) to the widget. */ private _applyTicketStatusUpdate; /** * Main poll function that polls based on current view */ private _poll; private _trackPollingEndpointReachability; /** * Track HTTP 429s on polling requests so the loop can back off. Any 2xx * clears the streak; a 429 lengthens the next poll delay exponentially. */ private _trackPollingRateLimit; /** * Handle view changes from the widget */ private _handleViewChange; /** * Handle ticket selection from the list */ private _handleSelectTicket; /** * Handle new conversation request */ private _handleNewConversation; /** * Handle back to tickets request */ private _handleBackToTickets; /** * Determine initial view based on ticket count */ private _determineInitialView; /** * Apply a fetched ticket list to internal state and return the appropriate view. * Shared by _determineInitialView (widget boot) and _loadTicketsAndReconcileView * (identity change at runtime). */ private _applyTicketsToState; /** * Start the self-scheduling poll loop. * * Idempotent: safe to call from any state-changing handler (send, restore, * identity change, ticket selection). The loop polls immediately, then * reschedules itself with a delay derived from widget state and rate-limit * backoff (see _nextPollDelayMs), and stops itself when there is nothing to * poll (see _hasSomethingToPoll). */ private _startPolling; private _pollThenSchedule; /** * Schedule the next poll, or stop the loop when there is nothing to poll. * An idle widget with no conversation generates zero polling traffic. */ private _scheduleNextPoll; /** * Delay before the next poll: exponential backoff while rate limited, * otherwise the open/closed cadence. */ private _nextPollDelayMs; /** * Whether the current view has anything worth polling for. Messages view * needs an active ticket; tickets view needs at least one ticket. The * restore-request view never polls. */ private _hasSomethingToPoll; /** * Stop the poll loop. */ private _stopPolling; /** * Re-attach the widget container if it has been detached from the DOM. * * SPA frameworks such as Turbo Drive (Hotwire) and Rails Turbo replace the * entire document.body on navigation. That removes our container — which was * appended to body in _renderWidget — without any teardown call, so the * widget silently disappears until a full page reload. Re-attaching the same * element (rather than re-rendering) preserves widget state and avoids * refetching messages. */ private _reattachWidgetIfDetached; /** * Start watching for the widget being detached from the DOM (e.g. by an SPA * navigation that replaces document.body) and re-attach it when it happens. */ private _startReattachWatcher; /** * Stop the DOM re-attach watcher. */ private _stopReattachWatcher; /** * Setup listener for identify events. * When user calls posthog.identify(), hide the identification form * since we now know who they are. */ private _setupIdentifyListener; /** * Show the widget (render it to DOM). * The widget respects its saved state (open/closed). * Note: Domain restrictions still apply - widget won't render on disallowed domains. */ show(): void; /** * Hide and remove the widget from the DOM. * Conversation data is preserved - call show() to re-render. */ hide(): void; /** * Check if the widget is currently visible (rendered in DOM) */ isVisible(): boolean; /** Get tickets list for the current widget session or verified identity */ getTickets(options?: GetTicketsOptions): Promise<GetTicketsResponse>; requestRestoreLink(email: string): Promise<RequestRestoreLinkResponse>; restoreFromToken(restoreToken: string): Promise<RestoreFromTokenResponse>; restoreFromUrlToken(): Promise<RestoreFromTokenResponse | null>; /** * Get the current active ticket ID * Returns null if no conversation has been started yet */ getCurrentTicketId(): string | null; /** * Get the widget session ID (persistent browser identifier) * This ID is used for access control and stays the same across page loads */ getWidgetSessionId(): string; private _identityFields; setIdentity(): void; clearIdentity(): void; private _resetConversationState; private _loadTicketsAndReconcileView; /** * Clean up the widget */ destroy(): void; /** * Reset all conversation data and destroy the widget. * Called on posthog.reset() to start fresh. */ reset(): void; /** * Render the widget to the DOM */ private _renderWidget; } /** * Initialize the conversations widget. * This is the entry point called from the lazy-loaded bundle. * * Singleton guard: only one ConversationsManager per page. The toolbar's * internal PostHog instance is excluded upstream (see loadIfEnabled), so * this always belongs to the customer's main instance. */ declare function initConversations(config: ConversationsRemoteConfig, posthog: PostHog): ConversationsManager; export { initConversations as default };