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:
Event | When It Fires | Use Case |
---|---|---|
onClick | User clicks anywhere in iframe | Track initial engagement, show loading states |
onStart | Practice session begins (mic activated) | Start timers, hide instructions, send analytics |
onStop | User ends session (before analysis) | Show “analyzing” message, calculate duration |
onSubmit | Analysis complete, results ready | Redirect 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
orCtrl+Shift+J
- Mac: Press
Cmd+Option+I
- Any browser: Right-click → “Inspect” → Console tab
Step 4: Interact with the Iframe
- Click the iframe - Watch for the
onClick
event - Start speaking - Watch for the
onStart
event (when mic activates) - End the session - Watch for the
onStop
event - 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:
Parameter | Effect | Example |
---|---|---|
bg=black | Set background color | bg=white , bg=transparent |
skipPrecheck=true | Skip audio/video checks | Faster initialization |
skipAutoStart=true | User must click to start | Gives time to read instructions |
countdown=false | Remove countdown timer | Instant start after click |
title=Custom+Title | Custom scenario title | Brand 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:
- Verify the iframe
src
includes a valid Tough Tongue AI URL - Check that iframe has proper
allow
permissions:allow="microphone; camera; display-capture"
- Ensure browser console is open before clicking
- 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.
Integration with Popular Frameworks
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:
- Checking
event.source
(which iframe sent it) - Checking scenario-specific data in the event payload
- 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:
- Saving the HTML logger as a file and opening in browser
- Or running a simple local server:
Then visit# Python 3 python -m http.server 8000 # Node.js npx serve .
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:
- Copy the events logger from this guide and save it locally
- Replace the scenario ID with your own (create scenarios at app.toughtongueai.com)
- Open the console and interact with the iframe
- 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
- API Documentation: Complete API reference with all endpoints
- Developer Tools: Interactive API playground and key management
- Simple Course Example: Open-source example using iframe events
- Book a Demo: Need help with custom integration? Let’s chat
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.