Module livekit.plugins.posthog.observer

Functions

def observe(session: Any,
api_key: str | None = None,
host: str | None = None,
session_id: str | None = None,
project_id: str | None = None) ‑> PostHogAgentObserver
Expand source code
def observe(
    session: Any,
    api_key: Optional[str] = None,
    host: Optional[str] = None,
    session_id: Optional[str] = None,
    project_id: Optional[str] = None,
) -> PostHogAgentObserver:
    """
    Convenience helper to initialize a PostHogAgentObserver and immediately attach it
    to an active LiveKit AgentSession or VoicePipelineAgent.
    """
    observer = PostHogAgentObserver(
        api_key=api_key,
        host=host,
        session_id=session_id,
        project_id=project_id,
    )
    observer.attach_to_session(session)
    return observer

Convenience helper to initialize a PostHogAgentObserver and immediately attach it to an active LiveKit AgentSession or VoicePipelineAgent.

Classes

class PostHogAgentObserver (api_key: str | None = None,
host: str | None = None,
session_id: str | None = None,
project_id: str | None = None)
Expand source code
class PostHogAgentObserver:
    """
    Subscribes to LiveKit Agent metrics and emits standardized PostHog events
    for AI observability ($ai_generation), conversational frustration (interruptions),
    voice latency waterfalls (TTS, STT, EOU), Session Replay cross-linking, and session costs.
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        host: Optional[str] = None,
        session_id: Optional[str] = None,
        project_id: Optional[str] = None,
    ):
        self.api_key = api_key or os.getenv("POSTHOG_API_KEY", "phc_mock_key_prism")
        self.host = host or os.getenv("POSTHOG_HOST", "https://us.i.posthog.com")
        self.session_id = session_id or f"RM_prism_{int(time.time())}"
        self.project_id = project_id or os.getenv("POSTHOG_PROJECT_ID", "default_proj")

        # Initialize PostHog client (flush_at=1 for real-time streaming telemetry)
        try:
            self.client = posthog.Posthog(
                project_api_key=self.api_key,
                host=self.host,
                flush_at=1,
                flush_interval=0.5,
            )
            logger.info("Initialized PostHogAgentObserver with session %s", self.session_id)
        except Exception as e:
            logger.warning("Failed to initialize PostHog client, logging locally: %s", e)
            self.client = None

        # Feature flags helper
        self.flags = PostHogFeatureFlags(client=self.client, default_distinct_id=self.session_id)

    def attach_to_session(self, session: Any):
        """Attaches metrics listener to a LiveKit AgentSession or VoicePipelineAgent."""
        if hasattr(session, "on"):
            session.on("metrics_collected", self.on_metrics_collected)
            logger.info("Attached PostHogAgentObserver to session.on('metrics_collected')")

    def link_session_replay(
        self,
        livekit_room: str,
        participant_identity: str,
        replay_id: Optional[str] = None,
    ) -> str:
        """
        Cross-links the LiveKit WebRTC voice session with PostHog Session Replay.
        Emits a $session_replay_link event so the voice recording is indexed alongside DOM replays.
        """
        rep_id = replay_id or self.session_id
        replay_url = f"{self.host.rstrip('/')}/project/{self.project_id}/replay/{rep_id}"

        properties = {
            "$session_id": self.session_id,
            "livekit_room": livekit_room,
            "participant_identity": participant_identity,
            "replay_url": replay_url,
            "webrtc_stream_linked": True,
            "timestamp": time.time(),
        }

        logger.info("[PostHog SessionReplay] Linked LiveKit room '%s' to replay: %s", livekit_room, replay_url)

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="$session_replay_link",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to emit $session_replay_link event: %s", e)

        return replay_url

    def on_metrics_collected(self, metrics: Any):
        """Dispatches collected metrics into PostHog telemetry."""
        metrics_type = getattr(metrics, "type", metrics.__class__.__name__)

        if "LLM" in metrics_type or hasattr(metrics, "input_tokens"):
            self.record_llm_metrics(metrics)
        elif "Interruption" in metrics_type or hasattr(metrics, "interrupted"):
            self.record_interruption_metrics(metrics)
        elif "TTS" in metrics_type or hasattr(metrics, "ttfb"):
            self.record_tts_metrics(metrics)
        elif "STT" in metrics_type:
            self.record_stt_metrics(metrics)
        elif "EOU" in metrics_type or hasattr(metrics, "end_of_utterance_delay"):
            self.record_eou_metrics(metrics)
        elif "Usage" in metrics_type or hasattr(metrics, "total_tokens"):
            self.record_session_usage(metrics)
        else:
            logger.debug("Captured general metric: %s", metrics_type)

    def record_llm_metrics(self, m: Any):
        """
        Conforms strictly to Contract C: PostHog AI Observability Schema ($ai_generation).
        """
        input_tokens = getattr(m, "input_tokens", getattr(m, "prompt_tokens", 0))
        output_tokens = getattr(m, "output_tokens", getattr(m, "completion_tokens", 0))
        duration = getattr(m, "duration", getattr(m, "latency", 0.0))
        ttft = getattr(m, "time_to_first_token", getattr(m, "ttft", 0.0))
        model = getattr(m, "model", "gpt-4o-mini")
        cancelled = getattr(m, "cancelled", False)

        tps = (output_tokens / duration) if (duration > 0 and output_tokens > 0) else 0.0

        properties = {
            "$session_id": self.session_id,
            "$ai_trace_id": getattr(m, "request_id", f"req_{int(time.time()*1000)}"),
            "$ai_model": model,
            "$ai_provider": "openai",
            "$ai_input_tokens": input_tokens,
            "$ai_output_tokens": output_tokens,
            "$ai_latency": round(duration, 4),
            "$ai_time_to_first_token": round(ttft, 4),
            "tokens_per_second": round(tps, 2),
            "cancelled": cancelled,
        }

        logger.info("[PostHog Telemetry] Emitting $ai_generation: tokens=(in:%d, out:%d) latency=%.2fs",
                    input_tokens, output_tokens, duration)

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="$ai_generation",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture $ai_generation event: %s", e)

    def record_interruption_metrics(self, m: Any):
        """Records conversational interruption / user frustration event."""
        properties = {
            "$session_id": self.session_id,
            "timestamp": time.time(),
            "speech_duration_before_cut": getattr(m, "duration", 0.0),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_interrupted")

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_interrupted",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_interrupted event: %s", e)

    def record_tts_metrics(self, m: Any):
        """Records Text-to-Speech synthesis latency waterfall metrics (TTFB, duration)."""
        properties = {
            "$session_id": self.session_id,
            "tts_ttfb": round(getattr(m, "ttfb", 0.0), 4),
            "tts_duration": round(getattr(m, "duration", 0.0), 4),
            "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
            "characters_count": getattr(m, "characters_count", 0),
            "tts_model": getattr(m, "label", "tts"),
            "cancelled": getattr(m, "cancelled", False),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_tts_latency: ttfb=%.3fs duration=%.3fs",
                    properties["tts_ttfb"], properties["tts_duration"])

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_tts_latency",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_tts_latency event: %s", e)

    def record_stt_metrics(self, m: Any):
        """Records Speech-to-Text transcription latency metrics."""
        properties = {
            "$session_id": self.session_id,
            "stt_duration": round(getattr(m, "duration", 0.0), 4),
            "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
            "stt_model": getattr(m, "label", "stt"),
            "streamed": getattr(m, "streamed", True),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_stt_latency: duration=%.3fs",
                    properties["stt_duration"])

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_stt_latency",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_stt_latency event: %s", e)

    def record_eou_metrics(self, m: Any):
        """Records End-of-Utterance (endpointing) conversational turn transition delay."""
        properties = {
            "$session_id": self.session_id,
            "eou_delay": round(getattr(m, "end_of_utterance_delay", 0.0), 4),
            "transcription_delay": round(getattr(m, "transcription_delay", 0.0), 4),
            "turn_completed_delay": round(getattr(m, "on_user_turn_completed_delay", 0.0), 4),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_endpointing_delay: eou_delay=%.3fs",
                    properties["eou_delay"])

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_endpointing_delay",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_endpointing_delay event: %s", e)

    def record_session_usage(self, m: Any):
        """Records cumulative session resource and token usage."""
        properties = {
            "$session_id": self.session_id,
            "total_tokens": getattr(m, "total_tokens", 0),
            "input_tokens": getattr(m, "input_tokens", 0),
            "output_tokens": getattr(m, "output_tokens", 0),
            "audio_duration": getattr(m, "audio_duration", 0.0),
        }

        logger.info("[PostHog Telemetry] Emitting voice_session_completed")

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_session_completed",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_session_completed event: %s", e)

    def shutdown(self):
        """Flushes remaining events to PostHog."""
        if self.client:
            try:
                self.client.flush()
            except Exception:
                pass

Subscribes to LiveKit Agent metrics and emits standardized PostHog events for AI observability ($ai_generation), conversational frustration (interruptions), voice latency waterfalls (TTS, STT, EOU), Session Replay cross-linking, and session costs.

Methods

def attach_to_session(self, session: Any)
Expand source code
def attach_to_session(self, session: Any):
    """Attaches metrics listener to a LiveKit AgentSession or VoicePipelineAgent."""
    if hasattr(session, "on"):
        session.on("metrics_collected", self.on_metrics_collected)
        logger.info("Attached PostHogAgentObserver to session.on('metrics_collected')")

Attaches metrics listener to a LiveKit AgentSession or VoicePipelineAgent.

Expand source code
def link_session_replay(
    self,
    livekit_room: str,
    participant_identity: str,
    replay_id: Optional[str] = None,
) -> str:
    """
    Cross-links the LiveKit WebRTC voice session with PostHog Session Replay.
    Emits a $session_replay_link event so the voice recording is indexed alongside DOM replays.
    """
    rep_id = replay_id or self.session_id
    replay_url = f"{self.host.rstrip('/')}/project/{self.project_id}/replay/{rep_id}"

    properties = {
        "$session_id": self.session_id,
        "livekit_room": livekit_room,
        "participant_identity": participant_identity,
        "replay_url": replay_url,
        "webrtc_stream_linked": True,
        "timestamp": time.time(),
    }

    logger.info("[PostHog SessionReplay] Linked LiveKit room '%s' to replay: %s", livekit_room, replay_url)

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="$session_replay_link",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to emit $session_replay_link event: %s", e)

    return replay_url

Cross-links the LiveKit WebRTC voice session with PostHog Session Replay. Emits a $session_replay_link event so the voice recording is indexed alongside DOM replays.

def on_metrics_collected(self, metrics: Any)
Expand source code
def on_metrics_collected(self, metrics: Any):
    """Dispatches collected metrics into PostHog telemetry."""
    metrics_type = getattr(metrics, "type", metrics.__class__.__name__)

    if "LLM" in metrics_type or hasattr(metrics, "input_tokens"):
        self.record_llm_metrics(metrics)
    elif "Interruption" in metrics_type or hasattr(metrics, "interrupted"):
        self.record_interruption_metrics(metrics)
    elif "TTS" in metrics_type or hasattr(metrics, "ttfb"):
        self.record_tts_metrics(metrics)
    elif "STT" in metrics_type:
        self.record_stt_metrics(metrics)
    elif "EOU" in metrics_type or hasattr(metrics, "end_of_utterance_delay"):
        self.record_eou_metrics(metrics)
    elif "Usage" in metrics_type or hasattr(metrics, "total_tokens"):
        self.record_session_usage(metrics)
    else:
        logger.debug("Captured general metric: %s", metrics_type)

Dispatches collected metrics into PostHog telemetry.

def record_eou_metrics(self, m: Any)
Expand source code
def record_eou_metrics(self, m: Any):
    """Records End-of-Utterance (endpointing) conversational turn transition delay."""
    properties = {
        "$session_id": self.session_id,
        "eou_delay": round(getattr(m, "end_of_utterance_delay", 0.0), 4),
        "transcription_delay": round(getattr(m, "transcription_delay", 0.0), 4),
        "turn_completed_delay": round(getattr(m, "on_user_turn_completed_delay", 0.0), 4),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_endpointing_delay: eou_delay=%.3fs",
                properties["eou_delay"])

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_endpointing_delay",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_endpointing_delay event: %s", e)

Records End-of-Utterance (endpointing) conversational turn transition delay.

def record_interruption_metrics(self, m: Any)
Expand source code
def record_interruption_metrics(self, m: Any):
    """Records conversational interruption / user frustration event."""
    properties = {
        "$session_id": self.session_id,
        "timestamp": time.time(),
        "speech_duration_before_cut": getattr(m, "duration", 0.0),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_interrupted")

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_interrupted",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_interrupted event: %s", e)

Records conversational interruption / user frustration event.

def record_llm_metrics(self, m: Any)
Expand source code
def record_llm_metrics(self, m: Any):
    """
    Conforms strictly to Contract C: PostHog AI Observability Schema ($ai_generation).
    """
    input_tokens = getattr(m, "input_tokens", getattr(m, "prompt_tokens", 0))
    output_tokens = getattr(m, "output_tokens", getattr(m, "completion_tokens", 0))
    duration = getattr(m, "duration", getattr(m, "latency", 0.0))
    ttft = getattr(m, "time_to_first_token", getattr(m, "ttft", 0.0))
    model = getattr(m, "model", "gpt-4o-mini")
    cancelled = getattr(m, "cancelled", False)

    tps = (output_tokens / duration) if (duration > 0 and output_tokens > 0) else 0.0

    properties = {
        "$session_id": self.session_id,
        "$ai_trace_id": getattr(m, "request_id", f"req_{int(time.time()*1000)}"),
        "$ai_model": model,
        "$ai_provider": "openai",
        "$ai_input_tokens": input_tokens,
        "$ai_output_tokens": output_tokens,
        "$ai_latency": round(duration, 4),
        "$ai_time_to_first_token": round(ttft, 4),
        "tokens_per_second": round(tps, 2),
        "cancelled": cancelled,
    }

    logger.info("[PostHog Telemetry] Emitting $ai_generation: tokens=(in:%d, out:%d) latency=%.2fs",
                input_tokens, output_tokens, duration)

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="$ai_generation",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture $ai_generation event: %s", e)

Conforms strictly to Contract C: PostHog AI Observability Schema ($ai_generation).

def record_session_usage(self, m: Any)
Expand source code
def record_session_usage(self, m: Any):
    """Records cumulative session resource and token usage."""
    properties = {
        "$session_id": self.session_id,
        "total_tokens": getattr(m, "total_tokens", 0),
        "input_tokens": getattr(m, "input_tokens", 0),
        "output_tokens": getattr(m, "output_tokens", 0),
        "audio_duration": getattr(m, "audio_duration", 0.0),
    }

    logger.info("[PostHog Telemetry] Emitting voice_session_completed")

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_session_completed",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_session_completed event: %s", e)

Records cumulative session resource and token usage.

def record_stt_metrics(self, m: Any)
Expand source code
def record_stt_metrics(self, m: Any):
    """Records Speech-to-Text transcription latency metrics."""
    properties = {
        "$session_id": self.session_id,
        "stt_duration": round(getattr(m, "duration", 0.0), 4),
        "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
        "stt_model": getattr(m, "label", "stt"),
        "streamed": getattr(m, "streamed", True),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_stt_latency: duration=%.3fs",
                properties["stt_duration"])

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_stt_latency",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_stt_latency event: %s", e)

Records Speech-to-Text transcription latency metrics.

def record_tts_metrics(self, m: Any)
Expand source code
def record_tts_metrics(self, m: Any):
    """Records Text-to-Speech synthesis latency waterfall metrics (TTFB, duration)."""
    properties = {
        "$session_id": self.session_id,
        "tts_ttfb": round(getattr(m, "ttfb", 0.0), 4),
        "tts_duration": round(getattr(m, "duration", 0.0), 4),
        "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
        "characters_count": getattr(m, "characters_count", 0),
        "tts_model": getattr(m, "label", "tts"),
        "cancelled": getattr(m, "cancelled", False),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_tts_latency: ttfb=%.3fs duration=%.3fs",
                properties["tts_ttfb"], properties["tts_duration"])

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_tts_latency",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_tts_latency event: %s", e)

Records Text-to-Speech synthesis latency waterfall metrics (TTFB, duration).

def shutdown(self)
Expand source code
def shutdown(self):
    """Flushes remaining events to PostHog."""
    if self.client:
        try:
            self.client.flush()
        except Exception:
            pass

Flushes remaining events to PostHog.

class Observer (api_key: str | None = None,
host: str | None = None,
session_id: str | None = None,
project_id: str | None = None)
Expand source code
class PostHogAgentObserver:
    """
    Subscribes to LiveKit Agent metrics and emits standardized PostHog events
    for AI observability ($ai_generation), conversational frustration (interruptions),
    voice latency waterfalls (TTS, STT, EOU), Session Replay cross-linking, and session costs.
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        host: Optional[str] = None,
        session_id: Optional[str] = None,
        project_id: Optional[str] = None,
    ):
        self.api_key = api_key or os.getenv("POSTHOG_API_KEY", "phc_mock_key_prism")
        self.host = host or os.getenv("POSTHOG_HOST", "https://us.i.posthog.com")
        self.session_id = session_id or f"RM_prism_{int(time.time())}"
        self.project_id = project_id or os.getenv("POSTHOG_PROJECT_ID", "default_proj")

        # Initialize PostHog client (flush_at=1 for real-time streaming telemetry)
        try:
            self.client = posthog.Posthog(
                project_api_key=self.api_key,
                host=self.host,
                flush_at=1,
                flush_interval=0.5,
            )
            logger.info("Initialized PostHogAgentObserver with session %s", self.session_id)
        except Exception as e:
            logger.warning("Failed to initialize PostHog client, logging locally: %s", e)
            self.client = None

        # Feature flags helper
        self.flags = PostHogFeatureFlags(client=self.client, default_distinct_id=self.session_id)

    def attach_to_session(self, session: Any):
        """Attaches metrics listener to a LiveKit AgentSession or VoicePipelineAgent."""
        if hasattr(session, "on"):
            session.on("metrics_collected", self.on_metrics_collected)
            logger.info("Attached PostHogAgentObserver to session.on('metrics_collected')")

    def link_session_replay(
        self,
        livekit_room: str,
        participant_identity: str,
        replay_id: Optional[str] = None,
    ) -> str:
        """
        Cross-links the LiveKit WebRTC voice session with PostHog Session Replay.
        Emits a $session_replay_link event so the voice recording is indexed alongside DOM replays.
        """
        rep_id = replay_id or self.session_id
        replay_url = f"{self.host.rstrip('/')}/project/{self.project_id}/replay/{rep_id}"

        properties = {
            "$session_id": self.session_id,
            "livekit_room": livekit_room,
            "participant_identity": participant_identity,
            "replay_url": replay_url,
            "webrtc_stream_linked": True,
            "timestamp": time.time(),
        }

        logger.info("[PostHog SessionReplay] Linked LiveKit room '%s' to replay: %s", livekit_room, replay_url)

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="$session_replay_link",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to emit $session_replay_link event: %s", e)

        return replay_url

    def on_metrics_collected(self, metrics: Any):
        """Dispatches collected metrics into PostHog telemetry."""
        metrics_type = getattr(metrics, "type", metrics.__class__.__name__)

        if "LLM" in metrics_type or hasattr(metrics, "input_tokens"):
            self.record_llm_metrics(metrics)
        elif "Interruption" in metrics_type or hasattr(metrics, "interrupted"):
            self.record_interruption_metrics(metrics)
        elif "TTS" in metrics_type or hasattr(metrics, "ttfb"):
            self.record_tts_metrics(metrics)
        elif "STT" in metrics_type:
            self.record_stt_metrics(metrics)
        elif "EOU" in metrics_type or hasattr(metrics, "end_of_utterance_delay"):
            self.record_eou_metrics(metrics)
        elif "Usage" in metrics_type or hasattr(metrics, "total_tokens"):
            self.record_session_usage(metrics)
        else:
            logger.debug("Captured general metric: %s", metrics_type)

    def record_llm_metrics(self, m: Any):
        """
        Conforms strictly to Contract C: PostHog AI Observability Schema ($ai_generation).
        """
        input_tokens = getattr(m, "input_tokens", getattr(m, "prompt_tokens", 0))
        output_tokens = getattr(m, "output_tokens", getattr(m, "completion_tokens", 0))
        duration = getattr(m, "duration", getattr(m, "latency", 0.0))
        ttft = getattr(m, "time_to_first_token", getattr(m, "ttft", 0.0))
        model = getattr(m, "model", "gpt-4o-mini")
        cancelled = getattr(m, "cancelled", False)

        tps = (output_tokens / duration) if (duration > 0 and output_tokens > 0) else 0.0

        properties = {
            "$session_id": self.session_id,
            "$ai_trace_id": getattr(m, "request_id", f"req_{int(time.time()*1000)}"),
            "$ai_model": model,
            "$ai_provider": "openai",
            "$ai_input_tokens": input_tokens,
            "$ai_output_tokens": output_tokens,
            "$ai_latency": round(duration, 4),
            "$ai_time_to_first_token": round(ttft, 4),
            "tokens_per_second": round(tps, 2),
            "cancelled": cancelled,
        }

        logger.info("[PostHog Telemetry] Emitting $ai_generation: tokens=(in:%d, out:%d) latency=%.2fs",
                    input_tokens, output_tokens, duration)

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="$ai_generation",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture $ai_generation event: %s", e)

    def record_interruption_metrics(self, m: Any):
        """Records conversational interruption / user frustration event."""
        properties = {
            "$session_id": self.session_id,
            "timestamp": time.time(),
            "speech_duration_before_cut": getattr(m, "duration", 0.0),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_interrupted")

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_interrupted",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_interrupted event: %s", e)

    def record_tts_metrics(self, m: Any):
        """Records Text-to-Speech synthesis latency waterfall metrics (TTFB, duration)."""
        properties = {
            "$session_id": self.session_id,
            "tts_ttfb": round(getattr(m, "ttfb", 0.0), 4),
            "tts_duration": round(getattr(m, "duration", 0.0), 4),
            "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
            "characters_count": getattr(m, "characters_count", 0),
            "tts_model": getattr(m, "label", "tts"),
            "cancelled": getattr(m, "cancelled", False),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_tts_latency: ttfb=%.3fs duration=%.3fs",
                    properties["tts_ttfb"], properties["tts_duration"])

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_tts_latency",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_tts_latency event: %s", e)

    def record_stt_metrics(self, m: Any):
        """Records Speech-to-Text transcription latency metrics."""
        properties = {
            "$session_id": self.session_id,
            "stt_duration": round(getattr(m, "duration", 0.0), 4),
            "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
            "stt_model": getattr(m, "label", "stt"),
            "streamed": getattr(m, "streamed", True),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_stt_latency: duration=%.3fs",
                    properties["stt_duration"])

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_stt_latency",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_stt_latency event: %s", e)

    def record_eou_metrics(self, m: Any):
        """Records End-of-Utterance (endpointing) conversational turn transition delay."""
        properties = {
            "$session_id": self.session_id,
            "eou_delay": round(getattr(m, "end_of_utterance_delay", 0.0), 4),
            "transcription_delay": round(getattr(m, "transcription_delay", 0.0), 4),
            "turn_completed_delay": round(getattr(m, "on_user_turn_completed_delay", 0.0), 4),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_endpointing_delay: eou_delay=%.3fs",
                    properties["eou_delay"])

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_endpointing_delay",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_endpointing_delay event: %s", e)

    def record_session_usage(self, m: Any):
        """Records cumulative session resource and token usage."""
        properties = {
            "$session_id": self.session_id,
            "total_tokens": getattr(m, "total_tokens", 0),
            "input_tokens": getattr(m, "input_tokens", 0),
            "output_tokens": getattr(m, "output_tokens", 0),
            "audio_duration": getattr(m, "audio_duration", 0.0),
        }

        logger.info("[PostHog Telemetry] Emitting voice_session_completed")

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_session_completed",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_session_completed event: %s", e)

    def shutdown(self):
        """Flushes remaining events to PostHog."""
        if self.client:
            try:
                self.client.flush()
            except Exception:
                pass

Subscribes to LiveKit Agent metrics and emits standardized PostHog events for AI observability ($ai_generation), conversational frustration (interruptions), voice latency waterfalls (TTS, STT, EOU), Session Replay cross-linking, and session costs.

Methods

def attach_to_session(self, session: Any)
Expand source code
def attach_to_session(self, session: Any):
    """Attaches metrics listener to a LiveKit AgentSession or VoicePipelineAgent."""
    if hasattr(session, "on"):
        session.on("metrics_collected", self.on_metrics_collected)
        logger.info("Attached PostHogAgentObserver to session.on('metrics_collected')")

Attaches metrics listener to a LiveKit AgentSession or VoicePipelineAgent.

Expand source code
def link_session_replay(
    self,
    livekit_room: str,
    participant_identity: str,
    replay_id: Optional[str] = None,
) -> str:
    """
    Cross-links the LiveKit WebRTC voice session with PostHog Session Replay.
    Emits a $session_replay_link event so the voice recording is indexed alongside DOM replays.
    """
    rep_id = replay_id or self.session_id
    replay_url = f"{self.host.rstrip('/')}/project/{self.project_id}/replay/{rep_id}"

    properties = {
        "$session_id": self.session_id,
        "livekit_room": livekit_room,
        "participant_identity": participant_identity,
        "replay_url": replay_url,
        "webrtc_stream_linked": True,
        "timestamp": time.time(),
    }

    logger.info("[PostHog SessionReplay] Linked LiveKit room '%s' to replay: %s", livekit_room, replay_url)

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="$session_replay_link",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to emit $session_replay_link event: %s", e)

    return replay_url

Cross-links the LiveKit WebRTC voice session with PostHog Session Replay. Emits a $session_replay_link event so the voice recording is indexed alongside DOM replays.

def on_metrics_collected(self, metrics: Any)
Expand source code
def on_metrics_collected(self, metrics: Any):
    """Dispatches collected metrics into PostHog telemetry."""
    metrics_type = getattr(metrics, "type", metrics.__class__.__name__)

    if "LLM" in metrics_type or hasattr(metrics, "input_tokens"):
        self.record_llm_metrics(metrics)
    elif "Interruption" in metrics_type or hasattr(metrics, "interrupted"):
        self.record_interruption_metrics(metrics)
    elif "TTS" in metrics_type or hasattr(metrics, "ttfb"):
        self.record_tts_metrics(metrics)
    elif "STT" in metrics_type:
        self.record_stt_metrics(metrics)
    elif "EOU" in metrics_type or hasattr(metrics, "end_of_utterance_delay"):
        self.record_eou_metrics(metrics)
    elif "Usage" in metrics_type or hasattr(metrics, "total_tokens"):
        self.record_session_usage(metrics)
    else:
        logger.debug("Captured general metric: %s", metrics_type)

Dispatches collected metrics into PostHog telemetry.

def record_eou_metrics(self, m: Any)
Expand source code
def record_eou_metrics(self, m: Any):
    """Records End-of-Utterance (endpointing) conversational turn transition delay."""
    properties = {
        "$session_id": self.session_id,
        "eou_delay": round(getattr(m, "end_of_utterance_delay", 0.0), 4),
        "transcription_delay": round(getattr(m, "transcription_delay", 0.0), 4),
        "turn_completed_delay": round(getattr(m, "on_user_turn_completed_delay", 0.0), 4),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_endpointing_delay: eou_delay=%.3fs",
                properties["eou_delay"])

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_endpointing_delay",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_endpointing_delay event: %s", e)

Records End-of-Utterance (endpointing) conversational turn transition delay.

def record_interruption_metrics(self, m: Any)
Expand source code
def record_interruption_metrics(self, m: Any):
    """Records conversational interruption / user frustration event."""
    properties = {
        "$session_id": self.session_id,
        "timestamp": time.time(),
        "speech_duration_before_cut": getattr(m, "duration", 0.0),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_interrupted")

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_interrupted",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_interrupted event: %s", e)

Records conversational interruption / user frustration event.

def record_llm_metrics(self, m: Any)
Expand source code
def record_llm_metrics(self, m: Any):
    """
    Conforms strictly to Contract C: PostHog AI Observability Schema ($ai_generation).
    """
    input_tokens = getattr(m, "input_tokens", getattr(m, "prompt_tokens", 0))
    output_tokens = getattr(m, "output_tokens", getattr(m, "completion_tokens", 0))
    duration = getattr(m, "duration", getattr(m, "latency", 0.0))
    ttft = getattr(m, "time_to_first_token", getattr(m, "ttft", 0.0))
    model = getattr(m, "model", "gpt-4o-mini")
    cancelled = getattr(m, "cancelled", False)

    tps = (output_tokens / duration) if (duration > 0 and output_tokens > 0) else 0.0

    properties = {
        "$session_id": self.session_id,
        "$ai_trace_id": getattr(m, "request_id", f"req_{int(time.time()*1000)}"),
        "$ai_model": model,
        "$ai_provider": "openai",
        "$ai_input_tokens": input_tokens,
        "$ai_output_tokens": output_tokens,
        "$ai_latency": round(duration, 4),
        "$ai_time_to_first_token": round(ttft, 4),
        "tokens_per_second": round(tps, 2),
        "cancelled": cancelled,
    }

    logger.info("[PostHog Telemetry] Emitting $ai_generation: tokens=(in:%d, out:%d) latency=%.2fs",
                input_tokens, output_tokens, duration)

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="$ai_generation",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture $ai_generation event: %s", e)

Conforms strictly to Contract C: PostHog AI Observability Schema ($ai_generation).

def record_session_usage(self, m: Any)
Expand source code
def record_session_usage(self, m: Any):
    """Records cumulative session resource and token usage."""
    properties = {
        "$session_id": self.session_id,
        "total_tokens": getattr(m, "total_tokens", 0),
        "input_tokens": getattr(m, "input_tokens", 0),
        "output_tokens": getattr(m, "output_tokens", 0),
        "audio_duration": getattr(m, "audio_duration", 0.0),
    }

    logger.info("[PostHog Telemetry] Emitting voice_session_completed")

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_session_completed",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_session_completed event: %s", e)

Records cumulative session resource and token usage.

def record_stt_metrics(self, m: Any)
Expand source code
def record_stt_metrics(self, m: Any):
    """Records Speech-to-Text transcription latency metrics."""
    properties = {
        "$session_id": self.session_id,
        "stt_duration": round(getattr(m, "duration", 0.0), 4),
        "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
        "stt_model": getattr(m, "label", "stt"),
        "streamed": getattr(m, "streamed", True),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_stt_latency: duration=%.3fs",
                properties["stt_duration"])

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_stt_latency",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_stt_latency event: %s", e)

Records Speech-to-Text transcription latency metrics.

def record_tts_metrics(self, m: Any)
Expand source code
def record_tts_metrics(self, m: Any):
    """Records Text-to-Speech synthesis latency waterfall metrics (TTFB, duration)."""
    properties = {
        "$session_id": self.session_id,
        "tts_ttfb": round(getattr(m, "ttfb", 0.0), 4),
        "tts_duration": round(getattr(m, "duration", 0.0), 4),
        "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
        "characters_count": getattr(m, "characters_count", 0),
        "tts_model": getattr(m, "label", "tts"),
        "cancelled": getattr(m, "cancelled", False),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_tts_latency: ttfb=%.3fs duration=%.3fs",
                properties["tts_ttfb"], properties["tts_duration"])

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_tts_latency",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_tts_latency event: %s", e)

Records Text-to-Speech synthesis latency waterfall metrics (TTFB, duration).

def shutdown(self)
Expand source code
def shutdown(self):
    """Flushes remaining events to PostHog."""
    if self.client:
        try:
            self.client.flush()
        except Exception:
            pass

Flushes remaining events to PostHog.

class AgentObserver (api_key: str | None = None,
host: str | None = None,
session_id: str | None = None,
project_id: str | None = None)
Expand source code
class PostHogAgentObserver:
    """
    Subscribes to LiveKit Agent metrics and emits standardized PostHog events
    for AI observability ($ai_generation), conversational frustration (interruptions),
    voice latency waterfalls (TTS, STT, EOU), Session Replay cross-linking, and session costs.
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        host: Optional[str] = None,
        session_id: Optional[str] = None,
        project_id: Optional[str] = None,
    ):
        self.api_key = api_key or os.getenv("POSTHOG_API_KEY", "phc_mock_key_prism")
        self.host = host or os.getenv("POSTHOG_HOST", "https://us.i.posthog.com")
        self.session_id = session_id or f"RM_prism_{int(time.time())}"
        self.project_id = project_id or os.getenv("POSTHOG_PROJECT_ID", "default_proj")

        # Initialize PostHog client (flush_at=1 for real-time streaming telemetry)
        try:
            self.client = posthog.Posthog(
                project_api_key=self.api_key,
                host=self.host,
                flush_at=1,
                flush_interval=0.5,
            )
            logger.info("Initialized PostHogAgentObserver with session %s", self.session_id)
        except Exception as e:
            logger.warning("Failed to initialize PostHog client, logging locally: %s", e)
            self.client = None

        # Feature flags helper
        self.flags = PostHogFeatureFlags(client=self.client, default_distinct_id=self.session_id)

    def attach_to_session(self, session: Any):
        """Attaches metrics listener to a LiveKit AgentSession or VoicePipelineAgent."""
        if hasattr(session, "on"):
            session.on("metrics_collected", self.on_metrics_collected)
            logger.info("Attached PostHogAgentObserver to session.on('metrics_collected')")

    def link_session_replay(
        self,
        livekit_room: str,
        participant_identity: str,
        replay_id: Optional[str] = None,
    ) -> str:
        """
        Cross-links the LiveKit WebRTC voice session with PostHog Session Replay.
        Emits a $session_replay_link event so the voice recording is indexed alongside DOM replays.
        """
        rep_id = replay_id or self.session_id
        replay_url = f"{self.host.rstrip('/')}/project/{self.project_id}/replay/{rep_id}"

        properties = {
            "$session_id": self.session_id,
            "livekit_room": livekit_room,
            "participant_identity": participant_identity,
            "replay_url": replay_url,
            "webrtc_stream_linked": True,
            "timestamp": time.time(),
        }

        logger.info("[PostHog SessionReplay] Linked LiveKit room '%s' to replay: %s", livekit_room, replay_url)

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="$session_replay_link",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to emit $session_replay_link event: %s", e)

        return replay_url

    def on_metrics_collected(self, metrics: Any):
        """Dispatches collected metrics into PostHog telemetry."""
        metrics_type = getattr(metrics, "type", metrics.__class__.__name__)

        if "LLM" in metrics_type or hasattr(metrics, "input_tokens"):
            self.record_llm_metrics(metrics)
        elif "Interruption" in metrics_type or hasattr(metrics, "interrupted"):
            self.record_interruption_metrics(metrics)
        elif "TTS" in metrics_type or hasattr(metrics, "ttfb"):
            self.record_tts_metrics(metrics)
        elif "STT" in metrics_type:
            self.record_stt_metrics(metrics)
        elif "EOU" in metrics_type or hasattr(metrics, "end_of_utterance_delay"):
            self.record_eou_metrics(metrics)
        elif "Usage" in metrics_type or hasattr(metrics, "total_tokens"):
            self.record_session_usage(metrics)
        else:
            logger.debug("Captured general metric: %s", metrics_type)

    def record_llm_metrics(self, m: Any):
        """
        Conforms strictly to Contract C: PostHog AI Observability Schema ($ai_generation).
        """
        input_tokens = getattr(m, "input_tokens", getattr(m, "prompt_tokens", 0))
        output_tokens = getattr(m, "output_tokens", getattr(m, "completion_tokens", 0))
        duration = getattr(m, "duration", getattr(m, "latency", 0.0))
        ttft = getattr(m, "time_to_first_token", getattr(m, "ttft", 0.0))
        model = getattr(m, "model", "gpt-4o-mini")
        cancelled = getattr(m, "cancelled", False)

        tps = (output_tokens / duration) if (duration > 0 and output_tokens > 0) else 0.0

        properties = {
            "$session_id": self.session_id,
            "$ai_trace_id": getattr(m, "request_id", f"req_{int(time.time()*1000)}"),
            "$ai_model": model,
            "$ai_provider": "openai",
            "$ai_input_tokens": input_tokens,
            "$ai_output_tokens": output_tokens,
            "$ai_latency": round(duration, 4),
            "$ai_time_to_first_token": round(ttft, 4),
            "tokens_per_second": round(tps, 2),
            "cancelled": cancelled,
        }

        logger.info("[PostHog Telemetry] Emitting $ai_generation: tokens=(in:%d, out:%d) latency=%.2fs",
                    input_tokens, output_tokens, duration)

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="$ai_generation",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture $ai_generation event: %s", e)

    def record_interruption_metrics(self, m: Any):
        """Records conversational interruption / user frustration event."""
        properties = {
            "$session_id": self.session_id,
            "timestamp": time.time(),
            "speech_duration_before_cut": getattr(m, "duration", 0.0),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_interrupted")

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_interrupted",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_interrupted event: %s", e)

    def record_tts_metrics(self, m: Any):
        """Records Text-to-Speech synthesis latency waterfall metrics (TTFB, duration)."""
        properties = {
            "$session_id": self.session_id,
            "tts_ttfb": round(getattr(m, "ttfb", 0.0), 4),
            "tts_duration": round(getattr(m, "duration", 0.0), 4),
            "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
            "characters_count": getattr(m, "characters_count", 0),
            "tts_model": getattr(m, "label", "tts"),
            "cancelled": getattr(m, "cancelled", False),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_tts_latency: ttfb=%.3fs duration=%.3fs",
                    properties["tts_ttfb"], properties["tts_duration"])

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_tts_latency",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_tts_latency event: %s", e)

    def record_stt_metrics(self, m: Any):
        """Records Speech-to-Text transcription latency metrics."""
        properties = {
            "$session_id": self.session_id,
            "stt_duration": round(getattr(m, "duration", 0.0), 4),
            "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
            "stt_model": getattr(m, "label", "stt"),
            "streamed": getattr(m, "streamed", True),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_stt_latency: duration=%.3fs",
                    properties["stt_duration"])

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_stt_latency",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_stt_latency event: %s", e)

    def record_eou_metrics(self, m: Any):
        """Records End-of-Utterance (endpointing) conversational turn transition delay."""
        properties = {
            "$session_id": self.session_id,
            "eou_delay": round(getattr(m, "end_of_utterance_delay", 0.0), 4),
            "transcription_delay": round(getattr(m, "transcription_delay", 0.0), 4),
            "turn_completed_delay": round(getattr(m, "on_user_turn_completed_delay", 0.0), 4),
        }

        logger.info("[PostHog Telemetry] Emitting voice_agent_endpointing_delay: eou_delay=%.3fs",
                    properties["eou_delay"])

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_agent_endpointing_delay",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_agent_endpointing_delay event: %s", e)

    def record_session_usage(self, m: Any):
        """Records cumulative session resource and token usage."""
        properties = {
            "$session_id": self.session_id,
            "total_tokens": getattr(m, "total_tokens", 0),
            "input_tokens": getattr(m, "input_tokens", 0),
            "output_tokens": getattr(m, "output_tokens", 0),
            "audio_duration": getattr(m, "audio_duration", 0.0),
        }

        logger.info("[PostHog Telemetry] Emitting voice_session_completed")

        if self.client:
            try:
                self.client.capture(
                    distinct_id=self.session_id,
                    event="voice_session_completed",
                    properties=properties,
                )
            except Exception as e:
                logger.error("Failed to capture voice_session_completed event: %s", e)

    def shutdown(self):
        """Flushes remaining events to PostHog."""
        if self.client:
            try:
                self.client.flush()
            except Exception:
                pass

Subscribes to LiveKit Agent metrics and emits standardized PostHog events for AI observability ($ai_generation), conversational frustration (interruptions), voice latency waterfalls (TTS, STT, EOU), Session Replay cross-linking, and session costs.

Methods

def attach_to_session(self, session: Any)
Expand source code
def attach_to_session(self, session: Any):
    """Attaches metrics listener to a LiveKit AgentSession or VoicePipelineAgent."""
    if hasattr(session, "on"):
        session.on("metrics_collected", self.on_metrics_collected)
        logger.info("Attached PostHogAgentObserver to session.on('metrics_collected')")

Attaches metrics listener to a LiveKit AgentSession or VoicePipelineAgent.

Expand source code
def link_session_replay(
    self,
    livekit_room: str,
    participant_identity: str,
    replay_id: Optional[str] = None,
) -> str:
    """
    Cross-links the LiveKit WebRTC voice session with PostHog Session Replay.
    Emits a $session_replay_link event so the voice recording is indexed alongside DOM replays.
    """
    rep_id = replay_id or self.session_id
    replay_url = f"{self.host.rstrip('/')}/project/{self.project_id}/replay/{rep_id}"

    properties = {
        "$session_id": self.session_id,
        "livekit_room": livekit_room,
        "participant_identity": participant_identity,
        "replay_url": replay_url,
        "webrtc_stream_linked": True,
        "timestamp": time.time(),
    }

    logger.info("[PostHog SessionReplay] Linked LiveKit room '%s' to replay: %s", livekit_room, replay_url)

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="$session_replay_link",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to emit $session_replay_link event: %s", e)

    return replay_url

Cross-links the LiveKit WebRTC voice session with PostHog Session Replay. Emits a $session_replay_link event so the voice recording is indexed alongside DOM replays.

def on_metrics_collected(self, metrics: Any)
Expand source code
def on_metrics_collected(self, metrics: Any):
    """Dispatches collected metrics into PostHog telemetry."""
    metrics_type = getattr(metrics, "type", metrics.__class__.__name__)

    if "LLM" in metrics_type or hasattr(metrics, "input_tokens"):
        self.record_llm_metrics(metrics)
    elif "Interruption" in metrics_type or hasattr(metrics, "interrupted"):
        self.record_interruption_metrics(metrics)
    elif "TTS" in metrics_type or hasattr(metrics, "ttfb"):
        self.record_tts_metrics(metrics)
    elif "STT" in metrics_type:
        self.record_stt_metrics(metrics)
    elif "EOU" in metrics_type or hasattr(metrics, "end_of_utterance_delay"):
        self.record_eou_metrics(metrics)
    elif "Usage" in metrics_type or hasattr(metrics, "total_tokens"):
        self.record_session_usage(metrics)
    else:
        logger.debug("Captured general metric: %s", metrics_type)

Dispatches collected metrics into PostHog telemetry.

def record_eou_metrics(self, m: Any)
Expand source code
def record_eou_metrics(self, m: Any):
    """Records End-of-Utterance (endpointing) conversational turn transition delay."""
    properties = {
        "$session_id": self.session_id,
        "eou_delay": round(getattr(m, "end_of_utterance_delay", 0.0), 4),
        "transcription_delay": round(getattr(m, "transcription_delay", 0.0), 4),
        "turn_completed_delay": round(getattr(m, "on_user_turn_completed_delay", 0.0), 4),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_endpointing_delay: eou_delay=%.3fs",
                properties["eou_delay"])

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_endpointing_delay",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_endpointing_delay event: %s", e)

Records End-of-Utterance (endpointing) conversational turn transition delay.

def record_interruption_metrics(self, m: Any)
Expand source code
def record_interruption_metrics(self, m: Any):
    """Records conversational interruption / user frustration event."""
    properties = {
        "$session_id": self.session_id,
        "timestamp": time.time(),
        "speech_duration_before_cut": getattr(m, "duration", 0.0),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_interrupted")

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_interrupted",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_interrupted event: %s", e)

Records conversational interruption / user frustration event.

def record_llm_metrics(self, m: Any)
Expand source code
def record_llm_metrics(self, m: Any):
    """
    Conforms strictly to Contract C: PostHog AI Observability Schema ($ai_generation).
    """
    input_tokens = getattr(m, "input_tokens", getattr(m, "prompt_tokens", 0))
    output_tokens = getattr(m, "output_tokens", getattr(m, "completion_tokens", 0))
    duration = getattr(m, "duration", getattr(m, "latency", 0.0))
    ttft = getattr(m, "time_to_first_token", getattr(m, "ttft", 0.0))
    model = getattr(m, "model", "gpt-4o-mini")
    cancelled = getattr(m, "cancelled", False)

    tps = (output_tokens / duration) if (duration > 0 and output_tokens > 0) else 0.0

    properties = {
        "$session_id": self.session_id,
        "$ai_trace_id": getattr(m, "request_id", f"req_{int(time.time()*1000)}"),
        "$ai_model": model,
        "$ai_provider": "openai",
        "$ai_input_tokens": input_tokens,
        "$ai_output_tokens": output_tokens,
        "$ai_latency": round(duration, 4),
        "$ai_time_to_first_token": round(ttft, 4),
        "tokens_per_second": round(tps, 2),
        "cancelled": cancelled,
    }

    logger.info("[PostHog Telemetry] Emitting $ai_generation: tokens=(in:%d, out:%d) latency=%.2fs",
                input_tokens, output_tokens, duration)

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="$ai_generation",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture $ai_generation event: %s", e)

Conforms strictly to Contract C: PostHog AI Observability Schema ($ai_generation).

def record_session_usage(self, m: Any)
Expand source code
def record_session_usage(self, m: Any):
    """Records cumulative session resource and token usage."""
    properties = {
        "$session_id": self.session_id,
        "total_tokens": getattr(m, "total_tokens", 0),
        "input_tokens": getattr(m, "input_tokens", 0),
        "output_tokens": getattr(m, "output_tokens", 0),
        "audio_duration": getattr(m, "audio_duration", 0.0),
    }

    logger.info("[PostHog Telemetry] Emitting voice_session_completed")

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_session_completed",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_session_completed event: %s", e)

Records cumulative session resource and token usage.

def record_stt_metrics(self, m: Any)
Expand source code
def record_stt_metrics(self, m: Any):
    """Records Speech-to-Text transcription latency metrics."""
    properties = {
        "$session_id": self.session_id,
        "stt_duration": round(getattr(m, "duration", 0.0), 4),
        "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
        "stt_model": getattr(m, "label", "stt"),
        "streamed": getattr(m, "streamed", True),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_stt_latency: duration=%.3fs",
                properties["stt_duration"])

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_stt_latency",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_stt_latency event: %s", e)

Records Speech-to-Text transcription latency metrics.

def record_tts_metrics(self, m: Any)
Expand source code
def record_tts_metrics(self, m: Any):
    """Records Text-to-Speech synthesis latency waterfall metrics (TTFB, duration)."""
    properties = {
        "$session_id": self.session_id,
        "tts_ttfb": round(getattr(m, "ttfb", 0.0), 4),
        "tts_duration": round(getattr(m, "duration", 0.0), 4),
        "audio_duration": round(getattr(m, "audio_duration", 0.0), 4),
        "characters_count": getattr(m, "characters_count", 0),
        "tts_model": getattr(m, "label", "tts"),
        "cancelled": getattr(m, "cancelled", False),
    }

    logger.info("[PostHog Telemetry] Emitting voice_agent_tts_latency: ttfb=%.3fs duration=%.3fs",
                properties["tts_ttfb"], properties["tts_duration"])

    if self.client:
        try:
            self.client.capture(
                distinct_id=self.session_id,
                event="voice_agent_tts_latency",
                properties=properties,
            )
        except Exception as e:
            logger.error("Failed to capture voice_agent_tts_latency event: %s", e)

Records Text-to-Speech synthesis latency waterfall metrics (TTFB, duration).

def shutdown(self)
Expand source code
def shutdown(self):
    """Flushes remaining events to PostHog."""
    if self.client:
        try:
            self.client.flush()
        except Exception:
            pass

Flushes remaining events to PostHog.