HTMX Frontend Logging
FRONTEND_LOGGING HTMX+JavaScript
Junior Edda · Updated 5 months, 1 week ago
Content
Skill: HTMX Frontend Logging
Capability Domain: FRONTEND_LOGGING
Technology Stack: HTMX+JavaScript
Overview
Implement frontend logging for HTMX interactions, JavaScript errors, and user actions. Logs are captured in the browser console and sent to a Django backend endpoint for persistence in logs/gui.log. Enables troubleshooting of frontend issues and tracking user interaction patterns.
Reference Implementation
Pattern 1: JavaScript Logger Class
Create static/js/logger.js:
/**
* Frontend Logger - logs to console and sends to backend for gui.log
*
* Usage:
* Logger.info('Button clicked', { button_id: 'save-btn', entity_id: 123 });
* Logger.error('Form validation failed', { form: 'playbook-create', errors: [...] });
*/
const Logger = {
/**
* Internal log method - sends to console and backend
*/
_log(level, message, context = {}) {
const timestamp = new Date().toISOString();
const logEntry = {
level: level.toUpperCase(),
timestamp: timestamp,
message: message,
context: context,
url: window.location.pathname,
user_agent: navigator.userAgent,
};
// Console output (always)
const consoleMethod = console[level] || console.log;
consoleMethod(
`[${level.toUpperCase()}] ${timestamp} ${message}`,
context
);
// Send to backend for gui.log (async, non-blocking)
this._sendToBackend(logEntry);
},
/**
* Send log entry to Django backend
*/
_sendToBackend(logEntry) {
fetch('/api/log/gui/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this._getCsrfToken(),
},
body: JSON.stringify(logEntry),
}).catch(err => {
// Don't log to backend again (avoid infinite loop)
console.error('Failed to send log to backend:', err);
});
},
/**
* Get CSRF token from DOM
*/
_getCsrfToken() {
const token = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
if (!token) {
// Try cookie fallback
const cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')[1];
return cookieValue || '';
}
return token;
},
/**
* Public logging methods
*/
debug(message, context) {
this._log('debug', message, context);
},
info(message, context) {
this._log('info', message, context);
},
warn(message, context) {
this._log('warn', message, context);
},
error(message, context) {
this._log('error', message, context);
},
};
// Make Logger globally available
window.Logger = Logger;
Pattern 2: HTMX Event Logging
Add HTMX event listeners to static/js/logger.js or separate file:
/**
* HTMX Event Logging
* Automatically logs all HTMX requests and responses
*/
// Log when HTMX request starts
document.body.addEventListener('htmx:beforeRequest', (event) => {
const target = event.detail.target;
const targetId = target.id || target.getAttribute('data-testid') || target.tagName;
Logger.info('HTMX request started', {
event: 'htmx:beforeRequest',
target: targetId,
method: event.detail.verb,
url: event.detail.path,
trigger: event.detail.triggeringEvent?.type,
});
});
// Log when HTMX request completes
document.body.addEventListener('htmx:afterRequest', (event) => {
const target = event.detail.target;
const targetId = target.id || target.getAttribute('data-testid') || target.tagName;
const successful = event.detail.successful;
const level = successful ? 'info' : 'error';
Logger[level]('HTMX request completed', {
event: 'htmx:afterRequest',
target: targetId,
method: event.detail.verb,
url: event.detail.path,
status: event.detail.xhr.status,
successful: successful,
elapsed_ms: event.detail.elapsed,
});
});
// Log HTMX response errors
document.body.addEventListener('htmx:responseError', (event) => {
const target = event.detail.target;
const targetId = target.id || target.getAttribute('data-testid') || target.tagName;
Logger.error('HTMX response error', {
event: 'htmx:responseError',
target: targetId,
method: event.detail.verb,
url: event.detail.path,
status: event.detail.xhr.status,
response_text: event.detail.xhr.responseText?.substring(0, 200), // Truncate
});
});
// Log HTMX swap events (DOM updates)
document.body.addEventListener('htmx:afterSwap', (event) => {
const target = event.detail.target;
const targetId = target.id || target.getAttribute('data-testid') || target.tagName;
Logger.debug('HTMX content swapped', {
event: 'htmx:afterSwap',
target: targetId,
swap_style: event.detail.xhr.getResponseHeader('HX-Reswap'),
});
});
// Log HTMX validation errors
document.body.addEventListener('htmx:validation:failed', (event) => {
const target = event.detail.target;
const targetId = target.id || target.getAttribute('data-testid') || target.tagName;
Logger.warn('HTMX validation failed', {
event: 'htmx:validation:failed',
target: targetId,
form: target.closest('form')?.id,
});
});
Pattern 3: User Action Logging
Log specific user actions:
/**
* User Action Logging
* Log important user interactions
*/
// Log button clicks
document.addEventListener('click', (event) => {
const button = event.target.closest('button, a.btn');
if (button) {
const action = button.getAttribute('data-action') ||
button.textContent.trim() ||
'unknown';
Logger.info('Button clicked', {
action: action,
button_id: button.id,
button_class: button.className,
test_id: button.getAttribute('data-testid'),
});
}
});
// Log form submissions
document.addEventListener('submit', (event) => {
const form = event.target;
Logger.info('Form submitted', {
form_id: form.id,
form_action: form.action,
form_method: form.method,
test_id: form.getAttribute('data-testid'),
});
});
// Log navigation
window.addEventListener('popstate', (event) => {
Logger.info('Navigation (back/forward)', {
url: window.location.pathname,
state: event.state,
});
});
Pattern 4: JavaScript Error Logging
Catch and log JavaScript errors:
/**
* Global Error Handler
* Catch unhandled JavaScript errors
*/
window.addEventListener('error', (event) => {
Logger.error('JavaScript error', {
message: event.message,
filename: event.filename,
line: event.lineno,
column: event.colno,
error: event.error?.toString(),
stack: event.error?.stack,
});
});
// Catch unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
Logger.error('Unhandled promise rejection', {
reason: event.reason?.toString(),
promise: event.promise,
});
});
Pattern 5: Django Backend Endpoint
Create view to receive frontend logs:
File: {app_name}/views/logging_views.py
import logging
import json
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
# GUI logger (writes to logs/gui.log)
gui_logger = logging.getLogger('gui')
@require_POST
def log_gui_event(request):
"""
Receive frontend log events and write to gui.log
Expected JSON payload:
{
"level": "INFO",
"timestamp": "2026-04-08T10:30:00.000Z",
"message": "Button clicked",
"context": {"button_id": "save-btn"},
"url": "/playbooks/create/",
"user_agent": "Mozilla/5.0..."
}
"""
try:
data = json.loads(request.body)
level = data.get('level', 'INFO').upper()
message = data.get('message', '')
context = data.get('context', {})
url = data.get('url', '')
user_agent = data.get('user_agent', '')
# Get user info
user_id = request.user.id if request.user.is_authenticated else 'anonymous'
# Format log message
log_message = (
f"[GUI] {message} | "
f"url={url} | "
f"user_id={user_id} | "
f"context={json.dumps(context)} | "
f"user_agent={user_agent[:50]}" # Truncate user agent
)
# Log at appropriate level
if level == 'DEBUG':
gui_logger.debug(log_message)
elif level == 'INFO':
gui_logger.info(log_message)
elif level == 'WARN':
gui_logger.warning(log_message)
elif level == 'ERROR':
gui_logger.error(log_message)
else:
gui_logger.info(log_message)
return JsonResponse({'status': 'logged'})
except json.JSONDecodeError as e:
logging.error(f"Failed to parse GUI log JSON: {e}")
return JsonResponse({'status': 'error', 'message': 'Invalid JSON'}, status=400)
except Exception as e:
logging.error(f"Failed to log GUI event: {e}", exc_info=True)
return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
Add URL route in {app_name}/urls.py:
from django.urls import path
from .views import logging_views
urlpatterns = [
# ... other URLs ...
path('api/log/gui/', logging_views.log_gui_event, name='log_gui_event'),
]
Pattern 6: Include Logger in Base Template
Add to templates/base.html:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... other head content ... -->
<!-- Logger (load early) -->
<script src="{% static 'js/logger.js' %}"></script>
</head>
<body>
<!-- ... body content ... -->
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- Initialize logging after HTMX loads -->
<script>
// Log page load
Logger.info('Page loaded', {
url: window.location.pathname,
referrer: document.referrer,
});
</script>
</body>
</html>
Pattern 7: Conditional Logging (Production vs Development)
Add environment-aware logging:
const Logger = {
// ... existing methods ...
/**
* Check if we should send logs to backend
*/
_shouldSendToBackend() {
// Don't send debug logs in production
if (this._isProduction() && this.level === 'debug') {
return false;
}
return true;
},
/**
* Check if running in production
*/
_isProduction() {
return window.location.hostname !== 'localhost' &&
window.location.hostname !== '127.0.0.1';
},
_log(level, message, context = {}) {
// ... existing console logging ...
// Only send to backend if appropriate
if (this._shouldSendToBackend()) {
this._sendToBackend(logEntry);
}
},
};
Usage Examples
Example 1: Log User Action
// In your template or JavaScript
document.getElementById('save-playbook-btn').addEventListener('click', () => {
Logger.info('Save playbook button clicked', {
playbook_id: document.getElementById('playbook-id').value,
form_valid: document.getElementById('playbook-form').checkValidity(),
});
});
Example 2: Log HTMX Form Submission
<form hx-post="/playbooks/create/"
hx-target="#playbook-list"
hx-swap="beforeend"
data-testid="playbook-create-form">
<!-- Form fields -->
<button type="submit" data-action="create-playbook">Create</button>
</form>
<script>
// HTMX events are automatically logged by the event listeners
// No additional code needed!
</script>
Example 3: Log Custom Business Logic
function validatePlaybookName(name) {
if (name.length < 3) {
Logger.warn('Playbook name validation failed', {
name: name,
min_length: 3,
actual_length: name.length,
});
return false;
}
Logger.debug('Playbook name validated', {
name: name,
length: name.length,
});
return true;
}
Example 4: Log API Call
async function fetchPlaybooks() {
Logger.info('Fetching playbooks', { endpoint: '/api/playbooks/' });
try {
const response = await fetch('/api/playbooks/');
const data = await response.json();
Logger.info('Playbooks fetched successfully', {
count: data.length,
endpoint: '/api/playbooks/',
});
return data;
} catch (error) {
Logger.error('Failed to fetch playbooks', {
endpoint: '/api/playbooks/',
error: error.message,
});
throw error;
}
}
Testing Frontend Logging
Manual Testing
- Open browser console
- Perform actions (click buttons, submit forms)
- Verify console shows log messages
- Check
logs/gui.logfor persisted entries
Automated Testing (Playwright)
# tests/e2e/test_frontend_logging.py
import pytest
from playwright.sync_api import Page
def test_button_click_logs(page: Page):
"""Verify button clicks are logged."""
# Set up console listener
console_messages = []
page.on('console', lambda msg: console_messages.append(msg.text))
# Navigate and click button
page.goto('/playbooks/')
page.click('[data-testid="create-playbook-btn"]')
# Verify log message
assert any('Button clicked' in msg for msg in console_messages)
def test_htmx_request_logs(page: Page):
"""Verify HTMX requests are logged."""
console_messages = []
page.on('console', lambda msg: console_messages.append(msg.text))
# Trigger HTMX request
page.goto('/playbooks/')
page.fill('[name="search"]', 'test')
page.wait_for_selector('[hx-trigger="input"]')
# Verify HTMX log messages
assert any('HTMX request started' in msg for msg in console_messages)
assert any('HTMX request completed' in msg for msg in console_messages)
Performance Considerations
Throttling Logs
For high-frequency events (e.g., scroll, mousemove), throttle logging:
const Logger = {
// ... existing methods ...
/**
* Throttled logging for high-frequency events
*/
_throttledLogs: new Map(),
throttle(key, message, context, delay = 1000) {
const now = Date.now();
const lastLog = this._throttledLogs.get(key);
if (!lastLog || now - lastLog > delay) {
this.info(message, context);
this._throttledLogs.set(key, now);
}
},
};
// Usage
window.addEventListener('scroll', () => {
Logger.throttle('scroll', 'User scrolling', {
scroll_y: window.scrollY,
});
});
Batching Logs
Send logs in batches to reduce HTTP requests:
const Logger = {
_logQueue: [],
_batchSize: 10,
_flushInterval: 5000, // 5 seconds
_sendToBackend(logEntry) {
this._logQueue.push(logEntry);
if (this._logQueue.length >= this._batchSize) {
this._flushLogs();
}
},
_flushLogs() {
if (this._logQueue.length === 0) return;
const batch = [...this._logQueue];
this._logQueue = [];
fetch('/api/log/gui/batch/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this._getCsrfToken(),
},
body: JSON.stringify({ logs: batch }),
}).catch(err => console.error('Failed to send log batch:', err));
},
};
// Flush logs periodically
setInterval(() => Logger._flushLogs(), Logger._flushInterval);
// Flush on page unload
window.addEventListener('beforeunload', () => Logger._flushLogs());
References
- HTMX Events Documentation: https://htmx.org/events/
- MDN Console API: https://developer.mozilla.org/en-US/docs/Web/API/Console
- Django Logging: https://docs.djangoproject.com/en/stable/topics/logging/
Details
- Capability Domain:
- FRONTEND_LOGGING
- Technology Stack:
- HTMX+JavaScript
- Created:
- 5 months, 1 week ago
- Updated:
- 5 months, 1 week ago
Playbook
Activities Using This Skill 0
No activities are currently linked to this skill.