posthog-js
Version:
Posthog-js allows you to automatically capture usage and send events to PostHog.
535 lines • 14.9 kB
TypeScript
import { ConversationsRemoteConfig } from "@posthog/browser-common";
import { PostHog } from './module';
//#region lib/src/posthog-conversations-types.d.ts
/**
* 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;
}
//#endregion
//#region lib/src/utils/globals.d.ts
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;
}
//#endregion
//#region lib/src/extensions/conversations/posthog-conversations.d.ts
type ConversationsManager$1 = LazyLoadedConversationsInterface;
//#endregion
//#region lib/src/extensions/conversations/external/index.d.ts
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 _lifecycleGeneration;
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;
//#endregion
export { initConversations as default };