| name | logging |
| description | Structured client-side logging and error reporting. Use when implementing console logging, error tracking, user session tracking, or analytics event patterns. |
| allowed-tools | Read, Write, Edit, Glob, Grep |
Logging Skill
Structured client-side logging, error reporting, and analytics event patterns.
Philosophy
- Logs should be structured - JSON-serializable for parsing
- Different environments need different verbosity - Filter by log level
- Privacy-aware - Never log PII or sensitive data
- Reliable delivery - Use sendBeacon for critical logs
Log Levels
Define a hierarchy of log severity:
/**
* @readonly
* @enum {number}
*/
const LogLevel = {
ERROR: 0, // Errors that need attention
WARN: 1, // Warnings that might indicate issues
INFO: 2, // General information
DEBUG: 3 // Detailed debugging info
};
/**
* Get log level from environment
* @returns {number}
*/
function getLogLevel() {
const level = window.__LOG_LEVEL__ || 'info';
return LogLevel[level.toUpperCase()] ?? LogLevel.INFO;
}
Environment Configuration
<!-- Set in HTML before scripts load -->
<script>
window.__LOG_LEVEL__ = 'debug'; // Development
// window.__LOG_LEVEL__ = 'warn'; // Production
</script>
Structured Logger
A logger that outputs JSON-serializable structured data:
/**
* @typedef {Object} LogEntry
* @property {string} level
* @property {string} message
* @property {number} timestamp
* @property {string} [sessionId]
* @property {string} [url]
* @property {Object} [data]
*/
/**
* Create a structured logger
* @param {Object} [options]
* @param {string} [options.sessionId]
* @param {number} [options.level]
*/
function createLogger(options = {}) {
const sessionId = options.sessionId || crypto.randomUUID();
const minLevel = options.level ?? getLogLevel();
/**
* Format log entry
* @param {string} level
* @param {string} message
* @param {Object} data
* @returns {LogEntry}
*/
function formatEntry(level, message, data = {}) {
return {
level,
message,
timestamp: Date.now(),
sessionId,
url: window.location.href,
...data
};
}
/**
* Output log entry
* @param {number} levelValue
* @param {string} levelName
* @param {string} message
* @param {Object} data
*/
function log(levelValue, levelName, message, data) {
if (levelValue > minLevel) return;
const entry = formatEntry(levelName, message, data);
// Development: pretty console output
if (minLevel >= LogLevel.DEBUG) {
const color = {
error: 'color: #ff5555',
warn: 'color: #ffaa00',
info: 'color: #5555ff',
debug: 'color: #888888'
}[levelName];
console.log(
`%c[${levelName.toUpperCase()}]`,
color,
message,
data
);
}
// Production: JSON output for log aggregation
if (minLevel < LogLevel.DEBUG) {
console.log(JSON.stringify(entry));
}
}
return {
/**
* Log error
* @param {string} message
* @param {Object} [data]
*/
error(message, data = {}) {
log(LogLevel.ERROR, 'error', message, data);
},
/**
* Log warning
* @param {string} message
* @param {Object} [data]
*/
warn(message, data = {}) {
log(LogLevel.WARN, 'warn', message, data);
},
/**
* Log info
* @param {string} message
* @param {Object} [data]
*/
info(message, data = {}) {
log(LogLevel.INFO, 'info', message, data);
},
/**
* Log debug
* @param {string} message
* @param {Object} [data]
*/
debug(message, data = {}) {
log(LogLevel.DEBUG, 'debug', message, data);
},
/**
* Get session ID
* @returns {string}
*/
getSessionId() {
return sessionId;
}
};
}
// Create default logger instance
const logger = createLogger();
Usage
logger.info('User logged in', { userId: 'user-123' });
logger.warn('API rate limit approaching', { remaining: 10 });
logger.error('Failed to save data', { error: error.message });
logger.debug('Component rendered', { props: { id: 123 } });
Console Enhancement (Development)
Enhanced console output for development:
/**
* Create a development-friendly console wrapper
*/
function createDevConsole() {
return {
/**
* Log with styled header
* @param {string} label
* @param {string} message
* @param {*} [data]
*/
labeled(label, message, data) {
console.log(
`%c ${label} %c ${message}`,
'background: #333; color: #fff; padding: 2px 6px; border-radius: 3px',
'color: inherit',
data ?? ''
);
},
/**
* Group related logs
* @param {string} label
* @param {() => void} fn
*/
group(label, fn) {
console.group(label);
fn();
console.groupEnd();
},
/**
* Display array/object as table
* @param {Array|Object} data
* @param {string[]} [columns]
*/
table(data, columns) {
console.table(data, columns);
},
/**
* Measure execution time
* @param {string} label
* @param {() => void} fn
*/
time(label, fn) {
console.time(label);
fn();
console.timeEnd(label);
},
/**
* Async timing
* @param {string} label
* @param {() => Promise<*>} fn
*/
async timeAsync(label, fn) {
console.time(label);
const result = await fn();
console.timeEnd(label);
return result;
}
};
}
const dev = createDevConsole();
Usage
dev.labeled('Router', 'Navigating to /about');
dev.group('User Data', () => {
console.log('Name:', user.name);
console.log('Email:', user.email);
});
dev.table(users, ['id', 'name', 'role']);
dev.time('Heavy computation', () => {
// expensive operation
});
Error Reporting
Send errors to a backend service reliably:
/**
* @typedef {Object} ErrorReport
* @property {string} message
* @property {string} [stack]
* @property {string} url
* @property {number} timestamp
* @property {string} sessionId
* @property {string} [userId]
* @property {Object} [context]
*/
/**
* Create an error reporter
* @param {Object} config
* @param {string} config.endpoint
* @param {string} [config.sessionId]
*/
function createErrorReporter(config) {
const { endpoint, sessionId = crypto.randomUUID() } = config;
let userId = null;
/**
* Set current user ID
* @param {string} id
*/
function setUser(id) {
userId = id;
}
/**
* Report an error
* @param {Error} error
* @param {Object} [context]
*/
function report(error, context = {}) {
/** @type {ErrorReport} */
const report = {
message: error.message,
stack: error.stack,
url: window.location.href,
timestamp: Date.now(),
sessionId,
userId,
context
};
// Use sendBeacon for reliability (works even during page unload)
const success = navigator.sendBeacon(
endpoint,
JSON.stringify(report)
);
if (!success) {
// Fallback to fetch (less reliable during unload)
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(report),
keepalive: true
}).catch(() => {
// Silently fail - we can't do much here
});
}
}
/**
* Set up global error handlers
*/
function installGlobalHandler() {
// Uncaught errors
window.addEventListener('error', (event) => {
report(event.error || new Error(event.message), {
type: 'uncaught',
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
const error = event.reason instanceof Error
? event.reason
: new Error(String(event.reason));
report(error, { type: 'unhandled_rejection' });
});
}
return {
report,
setUser,
installGlobalHandler
};
}
Usage
const errorReporter = createErrorReporter({
endpoint: '/api/errors'
});
// Install global handlers early
errorReporter.installGlobalHandler();
// Set user when they log in
errorReporter.setUser('user-123');
// Report errors manually
try {
dangerousOperation();
} catch (error) {
errorReporter.report(error, {
operation: 'dangerousOperation',
input: sanitizedInput
});
throw error;
}
Session Context
Track session information:
/**
* @typedef {Object} SessionContext
* @property {string} sessionId
* @property {string} [userId]
* @property {string} entryPage
* @property {string} referrer
* @property {string} userAgent
* @property {number} startTime
*/
/**
* Create session context
* @returns {SessionContext & {setUser: (id: string) => void}}
*/
function createSessionContext() {
const context = {
sessionId: crypto.randomUUID(),
userId: null,
entryPage: window.location.pathname,
referrer: document.referrer,
userAgent: navigator.userAgent,
startTime: Date.now()
};
return {
...context,
/**
* Set user ID after authentication
* @param {string} id
*/
setUser(id) {
context.userId = id;
},
/**
* Get session duration
* @returns {number} Duration in milliseconds
*/
getDuration() {
return Date.now() - context.startTime;
},
/**
* Get context for logging
* @returns {SessionContext}
*/
toJSON() {
return { ...context };
}
};
}
Analytics Events
Pattern for tracking user interactions:
/**
* @typedef {Object} AnalyticsEvent
* @property {string} name
* @property {Object} [properties]
* @property {number} timestamp
*/
/**
* Create an analytics tracker
* @param {Object} config
* @param {string} config.endpoint
* @param {boolean} [config.debug=false]
*/
function createAnalytics(config) {
const { endpoint, debug = false } = config;
const queue = [];
let flushTimer = null;
/**
* Queue event for sending
* @param {AnalyticsEvent} event
*/
function enqueue(event) {
queue.push(event);
// Debounce flush
if (flushTimer) clearTimeout(flushTimer);
flushTimer = setTimeout(flush, 1000);
}
/**
* Send queued events
*/
function flush() {
if (queue.length === 0) return;
const events = queue.splice(0);
navigator.sendBeacon(
endpoint,
JSON.stringify({ events })
);
}
// Flush on page unload
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
flush();
}
});
return {
/**
* Track an event
* @param {string} name
* @param {Object} [properties]
*/
track(name, properties = {}) {
const event = {
name,
properties,
timestamp: Date.now()
};
if (debug) {
console.log('[Analytics]', name, properties);
}
enqueue(event);
},
/**
* Track page view
* @param {string} [path]
*/
pageView(path = window.location.pathname) {
this.track('page_view', {
path,
referrer: document.referrer,
title: document.title
});
},
/**
* Track click
* @param {Element} element
* @param {string} [label]
*/
click(element, label) {
this.track('click', {
label: label || element.textContent?.trim().slice(0, 50),
elementType: element.tagName.toLowerCase(),
elementId: element.id || undefined
});
},
flush
};
}
Usage
const analytics = createAnalytics({
endpoint: '/api/analytics',
debug: true
});
// Page views
analytics.pageView();
// Button clicks
document.querySelector('#cta').addEventListener('click', (e) => {
analytics.click(e.target, 'CTA Button');
});
// Custom events
analytics.track('form_submitted', {
formId: 'contact',
fields: ['name', 'email', 'message']
});
Performance Logging
Track performance metrics:
/**
* Create a performance logger
*/
function createPerformanceLogger() {
return {
/**
* Mark a point in time
* @param {string} name
*/
mark(name) {
performance.mark(name);
},
/**
* Measure between two marks
* @param {string} name
* @param {string} startMark
* @param {string} [endMark]
* @returns {number} Duration in milliseconds
*/
measure(name, startMark, endMark) {
try {
const measure = performance.measure(name, startMark, endMark);
return measure.duration;
} catch {
return 0;
}
},
/**
* Time an operation
* @template T
* @param {string} name
* @param {() => T} fn
* @returns {T}
*/
time(name, fn) {
const start = performance.now();
const result = fn();
const duration = performance.now() - start;
if (duration > 100) {
logger.warn(`Slow operation: ${name}`, { duration });
}
return result;
},
/**
* Log Web Vitals
*/
logWebVitals() {
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1];
logger.info('LCP', { value: lcp.startTime });
}).observe({ type: 'largest-contentful-paint', buffered: true });
// FID
new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
logger.info('FID', { value: entry.processingStart - entry.startTime });
});
}).observe({ type: 'first-input', buffered: true });
// CLS
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
logger.info('CLS', { value: clsValue });
}).observe({ type: 'layout-shift', buffered: true });
}
};
}
Privacy Considerations
PII Scrubbing
/**
* Fields that should never be logged
*/
const SENSITIVE_FIELDS = [
'password',
'token',
'secret',
'apiKey',
'creditCard',
'ssn',
'authorization'
];
/**
* Scrub sensitive data from object
* @param {Object} data
* @returns {Object}
*/
function scrubSensitiveData(data) {
if (typeof data !== 'object' || data === null) {
return data;
}
const scrubbed = Array.isArray(data) ? [...data] : { ...data };
for (const key of Object.keys(scrubbed)) {
const lowerKey = key.toLowerCase();
if (SENSITIVE_FIELDS.some(field => lowerKey.includes(field.toLowerCase()))) {
scrubbed[key] = '[REDACTED]';
} else if (typeof scrubbed[key] === 'object') {
scrubbed[key] = scrubSensitiveData(scrubbed[key]);
}
}
return scrubbed;
}
/**
* Scrub emails from strings
* @param {string} str
* @returns {string}
*/
function scrubEmails(str) {
return str.replace(
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
'[EMAIL]'
);
}
Consent-Based Logging
/**
* Create a consent-aware logger
* @param {Object} logger - Base logger
*/
function createConsentLogger(logger) {
let analyticsConsent = false;
return {
...logger,
/**
* Set analytics consent
* @param {boolean} consent
*/
setConsent(consent) {
analyticsConsent = consent;
},
/**
* Log analytics event (requires consent)
* @param {string} name
* @param {Object} data
*/
analytics(name, data) {
if (!analyticsConsent) {
return;
}
logger.info(`analytics:${name}`, scrubSensitiveData(data));
}
};
}
Logging Checklist
When implementing logging:
- Log level configurable via environment
- All logs include timestamp
- Errors include stack trace
- No sensitive data (passwords, tokens, PII)
- sendBeacon used for critical error reporting
- Session ID tracked for correlation
- Performance logged for slow operations
- Analytics respect user consent
- Logs are structured (JSON-serializable)
Related Skills
- observability - Implement error tracking, performance monitoring, and use...
- javascript-author - Write vanilla JavaScript for Web Components with function...
- security - Write secure web pages and applications
- nodejs-backend - Build Node.js backend services with Express/Fastify, Post...