All blogs · Written by Ajitesh

How to Monitor Iframe Events in Tough Tongue AI: Complete Developer Guide

How to Monitor Iframe Events in Tough Tongue AI: Complete Developer Guide

When embedding Tough Tongue AI scenarios into your application, understanding iframe lifecycle events is crucial for creating seamless user experiences. Whether you’re building a custom training platform, integrating AI coaching into your LMS, or creating interactive learning experiences, you need visibility into what’s happening inside that iframe.

Today, I’ll show you exactly how to monitor all iframe events from Tough Tongue AI—a skill that’s essential for debugging, analytics integration, and building sophisticated user journeys.

Why Monitor Iframe Events?

Before we dive into the code, let’s understand why this matters:

1. User Journey Tracking

  • Know when users start practice sessions
  • Track completion rates
  • Measure engagement duration
  • Identify drop-off points

2. Analytics Integration

  • Send events to Google Analytics, Mixpanel, or Amplitude
  • Build custom dashboards
  • Track conversion funnels
  • Measure ROI on training programs

3. Custom UI/UX

  • Show loading states while session initializes
  • Display custom success messages on completion
  • Build progress indicators
  • Trigger follow-up actions (email, next lesson, etc.)

4. Debugging & Development

  • Understand event sequence and timing
  • Troubleshoot integration issues
  • Verify event payloads
  • Test different embedding configurations

The Tough Tongue AI Event System

Tough Tongue AI iframes emit four primary lifecycle events via the postMessage API:

EventWhen It FiresUse Case
onClickUser clicks anywhere in iframeTrack initial engagement, show loading states
onStartPractice session begins (mic activated)Start timers, hide instructions, send analytics
onStopUser ends session (before analysis)Show “analyzing” message, calculate duration
onSubmitAnalysis complete, results readyRedirect to dashboard, unlock next lesson, celebrate

Understanding this event flow is critical for building robust integrations.

The Simple Events Logger

Here’s a minimal HTML page that logs every iframe event. You can use it with w3schools Tryit Editor or save it locally:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Iframe Events Logger</title>
</head>
<body>
  <h2>🎯 Iframe Events Logger</h2>
  <p>Open the browser console (F12 or Cmd+Option+I) to see all iframe events.</p>

  <iframe
    id="targetIframe"
    src="https://app.toughtongueai.com/embed/676a419fae833c968b618f17?bg=black&skipPrecheck=true"
    width="100%"
    height="700px"
    frameborder="0"
    allow="microphone; camera; display-capture"
  ></iframe>

  <script>
    console.log('🚀 Iframe Events Logger Started');
    console.log('Listening for all postMessage events from iframe...\n');

    window.addEventListener('message', (event) => {
      console.log('─────────────────────────────────────');
      console.log('📨 MESSAGE RECEIVED');
      console.log('Origin:', event.origin);
      console.log('Data:', event.data);
      console.log('Type:', typeof event.data);
      console.log('Timestamp:', new Date().toISOString());
      
      if (typeof event.data === 'object') {
        console.log('Parsed Object:', JSON.stringify(event.data, null, 2));
      }
      
      console.log('─────────────────────────────────────\n');
    });

    const iframe = document.getElementById('targetIframe');
    
    iframe.addEventListener('load', () => {
      console.log('✅ Iframe loaded successfully');
    });

    iframe.addEventListener('error', (e) => {
      console.error('❌ Iframe error:', e);
    });

    console.log('👂 Event listeners attached. Waiting for events...\n');
  </script>
</body>
</html>

How to Use the Events Logger

Step 1: Save the HTML File

Copy the complete code above and save it as iframe-events-logger.html on your computer.

Step 2: Open in Your Browser

Simply double-click the file or drag it into your browser. You don’t need a web server for testing—this works locally.

Step 3: Open Developer Console

  • Windows/Linux: Press F12 or Ctrl+Shift+J
  • Mac: Press Cmd+Option+I
  • Any browser: Right-click → “Inspect” → Console tab

Step 4: Interact with the Iframe

  1. Click the iframe - Watch for the onClick event
  2. Start speaking - Watch for the onStart event (when mic activates)
  3. End the session - Watch for the onStop event
  4. View analysis - Watch for the onSubmit event (when results are ready)

You’ll see beautifully formatted console logs with timestamps, event types, and full data payloads.

Understanding the Console Output

When you interact with the embedded scenario, you’ll see output like this:

─────────────────────────────────────
📨 MESSAGE RECEIVED
Origin: https://app.toughtongueai.com
Data: { type: 'onClick' }
Type: object
Timestamp: 2025-10-02T14:32:18.451Z
Parsed Object: {
  "type": "onClick"
}
─────────────────────────────────────

Each event shows:

  • Origin: Where the message came from (should be toughtongueai.com)
  • Data: The event information (usually contains a type field)
  • Timestamp: When the event occurred

Real-World Integration Examples

Now let’s look at practical applications beyond logging.

Example 1: Google Analytics Integration

Track user progress through your embedded training:

window.addEventListener('message', (event) => {
  if (!event.origin.includes('toughtongueai.com')) return;

  const eventType = event.data?.type || event.data?.event || event.data;

  switch(eventType) {
    case 'onClick':
      gtag('event', 'iframe_click', {
        event_category: 'training',
        event_label: 'scenario_interaction'
      });
      break;
      
    case 'onStart':
      gtag('event', 'session_start', {
        event_category: 'training',
        event_label: 'practice_begun'
      });
      break;
      
    case 'onStop':
      gtag('event', 'session_stop', {
        event_category: 'training',
        event_label: 'practice_ended'
      });
      break;
      
    case 'onSubmit':
      gtag('event', 'session_complete', {
        event_category: 'training',
        event_label: 'analysis_viewed',
        value: 1
      });
      break;
  }
});

Example 2: Custom Loading States

Show contextual messages based on session progress:

const statusDiv = document.getElementById('status-message');
const iframe = document.getElementById('ttai-iframe');

window.addEventListener('message', (event) => {
  if (!event.origin.includes('toughtongueai.com')) return;

  const eventType = event.data?.type || event.data?.event || event.data;

  switch(eventType) {
    case 'onClick':
      statusDiv.textContent = '⏳ Initializing session...';
      statusDiv.className = 'status loading';
      break;
      
    case 'onStart':
      statusDiv.textContent = '🎤 Session active - practice in progress';
      statusDiv.className = 'status active';
      break;
      
    case 'onStop':
      statusDiv.textContent = '🧠 Analyzing your performance...';
      statusDiv.className = 'status analyzing';
      break;
      
    case 'onSubmit':
      statusDiv.textContent = '✅ Analysis complete!';
      statusDiv.className = 'status complete';
      
      // Auto-redirect after 3 seconds
      setTimeout(() => {
        window.location.href = '/dashboard';
      }, 3000);
      break;
  }
});

Example 3: Progress Persistence

Save progress to localStorage or your backend:

window.addEventListener('message', (event) => {
  if (!event.origin.includes('toughtongueai.com')) return;

  const eventType = event.data?.type || event.data?.event || event.data;
  const scenarioId = 'pm-interview-practice-001';

  switch(eventType) {
    case 'onStart':
      // Mark scenario as started
      fetch('/api/training/progress', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          scenario_id: scenarioId,
          status: 'in_progress',
          started_at: new Date().toISOString()
        })
      });
      break;
      
    case 'onSubmit':
      // Mark scenario as completed
      fetch('/api/training/progress', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          scenario_id: scenarioId,
          status: 'completed',
          completed_at: new Date().toISOString()
        })
      });
      
      // Unlock next lesson
      unlockNextLesson();
      break;
  }
});

Example 4: Session Duration Tracking

Measure how long users spend in practice sessions:

let sessionStartTime = null;
let sessionDuration = null;

window.addEventListener('message', (event) => {
  if (!event.origin.includes('toughtongueai.com')) return;

  const eventType = event.data?.type || event.data?.event || event.data;

  switch(eventType) {
    case 'onStart':
      sessionStartTime = Date.now();
      console.log('Session timer started');
      break;
      
    case 'onStop':
      if (sessionStartTime) {
        sessionDuration = Date.now() - sessionStartTime;
        const minutes = Math.floor(sessionDuration / 60000);
        const seconds = Math.floor((sessionDuration % 60000) / 1000);
        
        console.log(`Session duration: ${minutes}m ${seconds}s`);
        
        // Send to analytics
        gtag('event', 'timing_complete', {
          name: 'session_duration',
          value: sessionDuration,
          event_category: 'training'
        });
      }
      break;
  }
});

Common Customization Parameters

When embedding Tough Tongue AI, you can customize the iframe URL with these parameters:

ParameterEffectExample
bg=blackSet background colorbg=white, bg=transparent
skipPrecheck=trueSkip audio/video checksFaster initialization
skipAutoStart=trueUser must click to startGives time to read instructions
countdown=falseRemove countdown timerInstant start after click
title=Custom+TitleCustom scenario titleBrand your experience

Example URL with parameters:

https://app.toughtongueai.com/embed/SCENARIO_ID?bg=transparent&skipPrecheck=true&countdown=false

Troubleshooting Common Issues

Events Not Firing

Problem: No events appear in console

Solutions:

  1. Verify the iframe src includes a valid Tough Tongue AI URL
  2. Check that iframe has proper allow permissions:
    allow="microphone; camera; display-capture"
    
  3. Ensure browser console is open before clicking
  4. Check browser console for CORS or security errors

Wrong Origin

Problem: Getting messages from unexpected origins

Solution: Always verify the origin:

window.addEventListener('message', (event) => {
  // Security: Only process messages from Tough Tongue AI
  if (!event.origin.includes('toughtongueai.com')) {
    console.warn('Ignoring message from:', event.origin);
    return;
  }
  
  // Process event...
});

Events Firing Multiple Times

Problem: Same event fires twice or more

Cause: Multiple event listeners attached or parent page has multiple instances

Solution: Remove existing listeners before adding new ones:

function handleMessage(event) {
  if (!event.origin.includes('toughtongueai.com')) return;
  // Handle event...
}

// Remove if exists, then add
window.removeEventListener('message', handleMessage);
window.addEventListener('message', handleMessage);

Missing Event Data

Problem: event.data is undefined or empty

Possible causes:

  • Event fired before iframe fully loaded
  • Browser blocked the message (check console warnings)
  • Using an old embed URL format

Solution: Add error handling:

window.addEventListener('message', (event) => {
  if (!event.origin.includes('toughtongueai.com')) return;
  
  if (!event.data) {
    console.warn('Received empty event data');
    return;
  }
  
  // Safely access data
  const eventType = event.data?.type || event.data?.event || 'unknown';
  console.log('Event type:', eventType);
});

Security Best Practices

When working with iframe postMessage events, always follow these security guidelines:

1. Verify Origin

Always check that messages come from Tough Tongue AI:

window.addEventListener('message', (event) => {
  // CRITICAL: Verify origin
  if (event.origin !== 'https://app.toughtongueai.com') {
    console.warn('Rejected message from untrusted origin:', event.origin);
    return;
  }
  
  // Safe to process event
});

2. Validate Event Data

Don’t assume event data structure—validate it:

function isValidEvent(data) {
  if (!data || typeof data !== 'object') return false;
  
  const validTypes = ['onClick', 'onStart', 'onStop', 'onSubmit'];
  const eventType = data.type || data.event;
  
  return validTypes.includes(eventType);
}

window.addEventListener('message', (event) => {
  if (!event.origin.includes('toughtongueai.com')) return;
  
  if (!isValidEvent(event.data)) {
    console.warn('Invalid event data:', event.data);
    return;
  }
  
  // Process valid event
});

3. Sanitize Any User-Controlled Data

If you’re displaying event data in your UI, sanitize it:

function sanitizeHTML(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// Usage
statusDiv.innerHTML = sanitizeHTML(event.data.message);

4. Use HTTPS in Production

Always embed iframes over HTTPS in production environments. Most browsers will block mixed content (HTTP page loading HTTPS iframe) or vice versa.

React Example

import { useEffect, useState } from 'react';

function ToughTongueEmbed({ scenarioId }) {
  const [sessionState, setSessionState] = useState('idle');
  const [eventCount, setEventCount] = useState(0);

  useEffect(() => {
    const handleMessage = (event) => {
      if (!event.origin.includes('toughtongueai.com')) return;

      const eventType = event.data?.type || event.data?.event;
      
      setEventCount(prev => prev + 1);

      switch(eventType) {
        case 'onClick':
          setSessionState('initializing');
          break;
        case 'onStart':
          setSessionState('active');
          break;
        case 'onStop':
          setSessionState('analyzing');
          break;
        case 'onSubmit':
          setSessionState('complete');
          break;
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);

  return (
    <div className="training-container">
      <div className="stats">
        <p>Status: {sessionState}</p>
        <p>Events: {eventCount}</p>
      </div>
      <iframe
        src={`https://app.toughtongueai.com/embed/${scenarioId}?bg=white`}
        allow="microphone; camera; display-capture"
        style={{ width: '100%', height: '700px', border: 'none' }}
      />
    </div>
  );
}

Vue.js Example

<template>
  <div class="training-container">
    <div class="stats">
      <p>Status: {{ sessionState }}</p>
      <p>Events: {{ eventCount }}</p>
    </div>
    <iframe
      :src="iframeUrl"
      allow="microphone; camera; display-capture"
      style="width: 100%; height: 700px; border: none;"
    />
  </div>
</template>

<script>
export default {
  props: {
    scenarioId: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      sessionState: 'idle',
      eventCount: 0
    };
  },
  computed: {
    iframeUrl() {
      return `https://app.toughtongueai.com/embed/${this.scenarioId}?bg=white`;
    }
  },
  mounted() {
    window.addEventListener('message', this.handleMessage);
  },
  beforeUnmount() {
    window.removeEventListener('message', this.handleMessage);
  },
  methods: {
    handleMessage(event) {
      if (!event.origin.includes('toughtongueai.com')) return;

      const eventType = event.data?.type || event.data?.event;
      this.eventCount++;

      const stateMap = {
        'onClick': 'initializing',
        'onStart': 'active',
        'onStop': 'analyzing',
        'onSubmit': 'complete'
      };

      if (stateMap[eventType]) {
        this.sessionState = stateMap[eventType];
      }
    }
  }
};
</script>

Next.js Example

'use client';

import { useEffect, useState } from 'react';

export default function ToughTongueEmbed({ scenarioId }) {
  const [sessionState, setSessionState] = useState('idle');

  useEffect(() => {
    const handleMessage = (event) => {
      if (!event.origin.includes('toughtongueai.com')) return;

      const eventType = event.data?.type || event.data?.event;

      const stateMap = {
        'onClick': 'initializing',
        'onStart': 'active',
        'onStop': 'analyzing',
        'onSubmit': 'complete'
      };

      if (stateMap[eventType]) {
        setSessionState(stateMap[eventType]);
        
        // Track with Next.js analytics
        if (typeof window !== 'undefined' && window.gtag) {
          window.gtag('event', eventType, {
            event_category: 'training',
            scenario_id: scenarioId
          });
        }
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [scenarioId]);

  return (
    <div className="w-full">
      <div className="mb-4 p-4 bg-blue-50 rounded-lg">
        <p className="text-sm font-medium">Status: {sessionState}</p>
      </div>
      <iframe
        src={`https://app.toughtongueai.com/embed/${scenarioId}?bg=white`}
        allow="microphone; camera; display-capture"
        className="w-full h-[700px] border-0 rounded-lg shadow-lg"
      />
    </div>
  );
}

Performance Considerations

1. Event Throttling

If you’re tracking many rapid events, consider throttling:

function throttle(func, delay) {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func.apply(this, args);
    }
  };
}

const throttledAnalytics = throttle((eventType) => {
  // Send to analytics
  gtag('event', eventType);
}, 1000);

window.addEventListener('message', (event) => {
  // Process event immediately
  handleEvent(event);
  
  // Throttled analytics
  throttledAnalytics(event.data.type);
});

2. Cleanup Event Listeners

Always remove event listeners when component unmounts:

function setupEventListener() {
  const handler = (event) => {
    // Handle event
  };
  
  window.addEventListener('message', handler);
  
  // Return cleanup function
  return () => {
    window.removeEventListener('message', handler);
  };
}

// In React
useEffect(() => {
  return setupEventListener();
}, []);

3. Avoid Heavy Operations in Event Handlers

Keep event handlers lightweight:

// ❌ Bad: Heavy operation blocks UI
window.addEventListener('message', (event) => {
  processLargeDataSet(event.data); // Slow!
  updateUI();
});

// ✅ Good: Defer heavy work
window.addEventListener('message', (event) => {
  updateUI(); // Fast UI update
  
  // Defer heavy processing
  setTimeout(() => {
    processLargeDataSet(event.data);
  }, 0);
});

API Integration: Fetching Session Results

After a session completes (onSubmit event), you can fetch detailed results using the Tough Tongue AI API:

window.addEventListener('message', async (event) => {
  if (!event.origin.includes('toughtongueai.com')) return;

  if (event.data?.type === 'onSubmit') {
    const sessionId = event.data.sessionId; // If provided in event
    
    try {
      const response = await fetch(
        `https://app.toughtongueai.com/api/public/sessions/${sessionId}`,
        {
          headers: {
            'Authorization': `Bearer YOUR_API_TOKEN`,
            'Content-Type': 'application/json'
          }
        }
      );
      
      const sessionData = await response.json();
      
      console.log('Full transcript:', sessionData.transcript);
      console.log('Analysis:', sessionData.analysis);
      console.log('Score:', sessionData.score);
      
      // Display custom results UI
      displayCustomResults(sessionData);
      
    } catch (error) {
      console.error('Failed to fetch session results:', error);
    }
  }
});

For more details on the Sessions API, check out the Tough Tongue AI API documentation.

Frequently Asked Questions

Q: Do I need API credentials to monitor iframe events?

A: No! The postMessage events are available to any page embedding a Tough Tongue AI iframe—no API key required. You only need API credentials if you want to fetch detailed session data after completion.

Q: Can I customize which events are fired?

A: No, all events are automatically fired by the iframe. However, you can choose which events to listen for and respond to in your code. Simply ignore events you don’t need.

Q: Will events work on mobile devices?

A: Yes! The postMessage API works identically on mobile browsers. Just ensure your page is mobile-responsive and the iframe has proper dimensions.

Q: What if I’m using multiple iframes on one page?

A: The message event listener receives events from all iframes. You can differentiate by:

  1. Checking event.source (which iframe sent it)
  2. Checking scenario-specific data in the event payload
  3. Assigning unique IDs to each iframe and tracking separately

Example:

const iframe1 = document.getElementById('scenario-1');
const iframe2 = document.getElementById('scenario-2');

window.addEventListener('message', (event) => {
  if (event.source === iframe1.contentWindow) {
    console.log('Event from scenario 1:', event.data);
  } else if (event.source === iframe2.contentWindow) {
    console.log('Event from scenario 2:', event.data);
  }
});

Q: Can I send messages TO the iframe?

A: Currently, the event system is one-directional (iframe → parent). The iframe emits lifecycle events, but doesn’t accept control messages from the parent. For controlling scenario behavior, use URL parameters like skipAutoStart, countdown, etc.

Q: How do I test events in a local development environment?

A: You can test locally by:

  1. Saving the HTML logger as a file and opening in browser
  2. Or running a simple local server:
    # Python 3
    python -m http.server 8000
    
    # Node.js
    npx serve .
    
    Then visit http://localhost:8000

Q: Are there rate limits on events?

A: No, there are no rate limits on receiving iframe events. They fire naturally based on user interaction. However, if you’re sending events to external analytics platforms, respect their rate limits.

Q: Can I prevent the onSubmit event from firing?

A: No, lifecycle events are automatic and cannot be prevented. However, you can choose not to listen for specific events or ignore them in your handler.

Conclusion: Build Smarter Training Experiences

Monitoring iframe events unlocks powerful possibilities:

Analytics: Track user engagement and completion rates
UX: Show contextual loading states and progress indicators
Automation: Trigger follow-up actions like unlocking content or sending emails
Integration: Connect training data to your LMS, CRM, or custom platform
Debugging: Understand exactly what’s happening during development

Whether you’re building a corporate training platform, an educational product, or embedding AI coaching into your application, understanding these events is essential for creating polished, professional experiences.

Ready to Build?

Start by:

  1. Copy the events logger from this guide and save it locally
  2. Replace the scenario ID with your own (create scenarios at app.toughtongueai.com)
  3. Open the console and interact with the iframe
  4. Watch the events flow and build from there

Need more advanced features like generating scenario access tokens or fetching session transcripts? Check out the Tough Tongue AI Developer Tools.

Additional Resources


Have questions about iframe events or need help with your integration? Reach out at help@getarchieai.com or join our Discord community.


About the Author

Ajitesh is the CEO & Co-founder of Tough Tongue AI, an AI-powered conversational training platform. Previously a PM at Google working on Gemini, he’s passionate about making high-stakes communication skills accessible through AI.

LinkedIn | Twitter


Related Posts