Real-time notifications and alerts with WSX
<!DOCTYPE html>
<html>
<head>
<title>WSX Notifications System</title>
<script src="/wsx.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 300px;
gap: 20px;
}
.main-content {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.notification-panel {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
height: fit-content;
}
/* Toast Notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
min-width: 300px;
padding: 16px;
border-radius: 6px;
color: white;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transform: translateX(400px);
opacity: 0;
animation: slideIn 0.3s ease forwards;
position: relative;
}
.toast.removing {
animation: slideOut 0.3s ease forwards;
}
.toast-success {
background: #10b981;
}
.toast-error {
background: #ef4444;
}
.toast-warning {
background: #f59e0b;
}
.toast-info {
background: #3b82f6;
}
.toast-close {
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
}
.toast-close:hover {
background: rgba(255, 255, 255, 0.2);
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
/* Notification Panel */
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.notification-badge {
background: #ef4444;
color: white;
border-radius: 50%;
padding: 4px 8px;
font-size: 12px;
font-weight: bold;
}
.notification-list {
max-height: 400px;
overflow-y: auto;
}
.notification-item {
padding: 12px;
border-bottom: 1px solid #e5e7eb;
cursor: pointer;
transition: background-color 0.2s;
}
.notification-item:hover {
background-color: #f9fafb;
}
.notification-item.unread {
background-color: #eff6ff;
border-left: 4px solid #3b82f6;
}
.notification-item:last-child {
border-bottom: none;
}
.notification-title {
font-weight: 500;
margin-bottom: 4px;
color: #111827;
}
.notification-message {
font-size: 14px;
color: #6b7280;
margin-bottom: 4px;
}
.notification-time {
font-size: 12px;
color: #9ca3af;
}
/* System Alerts */
.system-alert {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 6px;
padding: 16px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
}
.system-alert.error {
background: #fee2e2;
border-color: #ef4444;
}
.system-alert.success {
background: #d1fae5;
border-color: #10b981;
}
.system-alert.info {
background: #dbeafe;
border-color: #3b82f6;
}
.alert-icon {
font-size: 20px;
}
.alert-content {
flex: 1;
}
.alert-title {
font-weight: 500;
margin-bottom: 4px;
}
.alert-message {
font-size: 14px;
color: #6b7280;
}
.alert-close {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
}
/* Controls */
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.btn {
padding: 10px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-secondary {
background: #e5e7eb;
color: #374151;
}
.btn-secondary:hover {
background: #d1d5db;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-warning {
background: #f59e0b;
color: white;
}
.btn-warning:hover {
background: #d97706;
}
</style>
</head>
<body>
<div wx-config='{"url": "ws://localhost:3000/ws"}'>
<!-- Toast Container -->
<div id="toast-container" class="toast-container"></div>
<!-- System Alerts -->
<div id="system-alerts"></div>
<div class="container">
<div class="main-content">
<h1>Notification System Demo</h1>
<div class="controls">
<button
class="btn btn-success"
wx-send="show-toast"
wx-target="#toast-container"
data-type="success"
>
Success Toast
</button>
<button
class="btn btn-danger"
wx-send="show-toast"
wx-target="#toast-container"
data-type="error"
>
Error Toast
</button>
<button
class="btn btn-warning"
wx-send="show-toast"
wx-target="#toast-container"
data-type="warning"
>
Warning Toast
</button>
<button
class="btn btn-primary"
wx-send="show-toast"
wx-target="#toast-container"
data-type="info"
>
Info Toast
</button>
</div>
<div class="controls">
<button
class="btn btn-secondary"
wx-send="show-system-alert"
wx-target="#system-alerts"
data-type="info"
>
System Alert
</button>
<button
class="btn btn-secondary"
wx-send="broadcast-notification"
wx-target="#system-alerts"
>
Broadcast to All
</button>
<button
class="btn btn-secondary"
wx-send="send-private-message"
wx-target="#system-alerts"
>
Send Private Message
</button>
</div>
<h2>Activity Feed</h2>
<div id="activity-feed">
<p>Activity will appear here...</p>
</div>
</div>
<div class="notification-panel">
<div class="notification-header">
<h3>Notifications</h3>
<span
id="notification-badge"
class="notification-badge"
style="display: none;"
>0</span
>
</div>
<div class="controls">
<button
class="btn btn-secondary"
wx-send="mark-all-read"
wx-target="#notification-list"
>
Mark All Read
</button>
<button
class="btn btn-secondary"
wx-send="clear-notifications"
wx-target="#notification-list"
>
Clear All
</button>
</div>
<div id="notification-list" class="notification-list">
<!-- Notifications will be populated here -->
</div>
</div>
</div>
</div>
<script>
// Auto-remove toasts after 5 seconds
function autoRemoveToast(toastElement) {
setTimeout(() => {
toastElement.classList.add("removing");
setTimeout(() => {
toastElement.remove();
}, 300);
}, 5000);
}
// Handle toast close buttons
document.addEventListener("click", (e) => {
if (e.target.classList.contains("toast-close")) {
const toast = e.target.closest(".toast");
toast.classList.add("removing");
setTimeout(() => {
toast.remove();
}, 300);
}
});
// Handle alert close buttons
document.addEventListener("click", (e) => {
if (e.target.classList.contains("alert-close")) {
const alert = e.target.closest(".system-alert");
alert.remove();
}
});
// Auto-load notifications on page load
window.addEventListener("load", () => {
window.wsx.send("load-notifications", "#notification-list");
});
</script>
</body>
</html>
import { createExpressWSXServer } from "@wsx-sh/express";
import express from "express";
const wsx = createExpressWSXServer();
const app = wsx.getApp();
// Notification storage
const notifications = new Map(); // userId -> notifications array
const systemMessages = [];
// Mock user data
const users = [
{ id: 1, name: "John Doe", email: "john@example.com" },
{ id: 2, name: "Jane Smith", email: "jane@example.com" },
{ id: 3, name: "Bob Johnson", email: "bob@example.com" },
];
// Initialize mock notifications
function initMockNotifications() {
users.forEach((user) => {
notifications.set(user.id, [
{
id: Date.now() + Math.random(),
title: "Welcome!",
message: "Thanks for joining our platform",
type: "info",
read: false,
timestamp: new Date(Date.now() - 3600000).toISOString(),
},
{
id: Date.now() + Math.random(),
title: "New Feature Available",
message: "Check out our new dashboard widgets",
type: "info",
read: false,
timestamp: new Date(Date.now() - 7200000).toISOString(),
},
]);
});
}
initMockNotifications();
// Show toast notification
wsx.on("show-toast", async (request, connection) => {
const type = request.data.type || "info";
const messages = {
success: "Operation completed successfully!",
error: "An error occurred. Please try again.",
warning: "Please check your input and try again.",
info: "This is an informational message.",
};
const toastId = Date.now() + Math.random();
const message = messages[type];
const toastHtml = `
<div class="toast toast-${type}" data-toast-id="${toastId}">
<div>${message}</div>
<button class="toast-close">×</button>
</div>
`;
return {
id: request.id,
target: request.target,
html: toastHtml,
swap: "beforeend",
};
});
// Show system alert
wsx.on("show-system-alert", async (request, connection) => {
const type = request.data.type || "info";
const alerts = {
info: {
icon: "ℹ️",
title: "System Information",
message: "Scheduled maintenance tonight at 2 AM EST",
},
warning: {
icon: "⚠️",
title: "System Warning",
message: "High server load detected",
},
error: {
icon: "❌",
title: "System Error",
message: "Database connection temporarily unavailable",
},
success: {
icon: "✅",
title: "System Update",
message: "All systems are operating normally",
},
};
const alert = alerts[type];
const alertHtml = `
<div class="system-alert ${type}">
<div class="alert-icon">${alert.icon}</div>
<div class="alert-content">
<div class="alert-title">${alert.title}</div>
<div class="alert-message">${alert.message}</div>
</div>
<button class="alert-close">×</button>
</div>
`;
return {
id: request.id,
target: request.target,
html: alertHtml,
swap: "beforeend",
};
});
// Broadcast notification to all users
wsx.on("broadcast-notification", async (request, connection) => {
const user = connection.sessionData?.user || { name: "Anonymous" };
const message = {
id: Date.now() + Math.random(),
title: "Broadcast Message",
message: `${user.name} sent a broadcast message to all users`,
type: "info",
read: false,
timestamp: new Date().toISOString(),
};
// Add to all users' notifications
users.forEach((user) => {
const userNotifications = notifications.get(user.id) || [];
userNotifications.unshift(message);
notifications.set(user.id, userNotifications);
});
// Broadcast to all connected clients
const connections = wsx.getConnections();
connections.forEach((conn) => {
// Send toast notification
const toastHtml = `
<div class="toast toast-info">
<div>📢 ${message.message}</div>
<button class="toast-close">×</button>
</div>
`;
wsx.sendToConnection(conn.id, "#toast-container", toastHtml, "beforeend");
// Update notification list
const notificationHtml = generateNotificationHtml(message);
wsx.sendToConnection(
conn.id,
"#notification-list",
notificationHtml,
"afterbegin"
);
});
return {
id: request.id,
target: request.target,
html: `
<div class="system-alert success">
<div class="alert-icon">✅</div>
<div class="alert-content">
<div class="alert-title">Broadcast Sent</div>
<div class="alert-message">Message sent to all users</div>
</div>
<button class="alert-close">×</button>
</div>
`,
swap: "beforeend",
};
});
// Send private message
wsx.on("send-private-message", async (request, connection) => {
const sender = connection.sessionData?.user || { name: "Anonymous" };
const targetUser = users[Math.floor(Math.random() * users.length)];
const message = {
id: Date.now() + Math.random(),
title: "Private Message",
message: `${sender.name} sent you a private message`,
type: "info",
read: false,
timestamp: new Date().toISOString(),
};
// Add to target user's notifications
const userNotifications = notifications.get(targetUser.id) || [];
userNotifications.unshift(message);
notifications.set(targetUser.id, userNotifications);
// Find target user's connection
const targetConnection = wsx
.getConnections()
.find((conn) => conn.sessionData?.user?.id === targetUser.id);
if (targetConnection) {
// Send toast notification
const toastHtml = `
<div class="toast toast-info">
<div>💬 ${message.message}</div>
<button class="toast-close">×</button>
</div>
`;
wsx.sendToConnection(
targetConnection.id,
"#toast-container",
toastHtml,
"beforeend"
);
// Update notification list
const notificationHtml = generateNotificationHtml(message);
wsx.sendToConnection(
targetConnection.id,
"#notification-list",
notificationHtml,
"afterbegin"
);
}
return {
id: request.id,
target: request.target,
html: `
<div class="system-alert success">
<div class="alert-icon">✅</div>
<div class="alert-content">
<div class="alert-title">Message Sent</div>
<div class="alert-message">Private message sent to ${targetUser.name}</div>
</div>
<button class="alert-close">×</button>
</div>
`,
swap: "beforeend",
};
});
// Load notifications
wsx.on("load-notifications", async (request, connection) => {
const user = connection.sessionData?.user || users[0]; // Default to first user for demo
const userNotifications = notifications.get(user.id) || [];
if (userNotifications.length === 0) {
return {
id: request.id,
target: request.target,
html: `<div class="notification-item">No notifications yet</div>`,
};
}
const notificationsHtml = userNotifications
.map(generateNotificationHtml)
.join("");
const unreadCount = userNotifications.filter((n) => !n.read).length;
return [
{
id: request.id,
target: request.target,
html: notificationsHtml,
},
{
id: request.id,
target: "#notification-badge",
html:
unreadCount > 0
? `<span class="notification-badge">${unreadCount}</span>`
: "",
swap: "outerHTML",
},
];
});
// Mark all notifications as read
wsx.on("mark-all-read", async (request, connection) => {
const user = connection.sessionData?.user || users[0];
const userNotifications = notifications.get(user.id) || [];
userNotifications.forEach((notification) => {
notification.read = true;
});
const notificationsHtml = userNotifications
.map(generateNotificationHtml)
.join("");
return [
{
id: request.id,
target: request.target,
html: notificationsHtml,
},
{
id: request.id,
target: "#notification-badge",
html: "",
swap: "outerHTML",
},
];
});
// Clear all notifications
wsx.on("clear-notifications", async (request, connection) => {
const user = connection.sessionData?.user || users[0];
notifications.set(user.id, []);
return [
{
id: request.id,
target: request.target,
html: `<div class="notification-item">No notifications yet</div>`,
},
{
id: request.id,
target: "#notification-badge",
html: "",
swap: "outerHTML",
},
];
});
// Helper function to generate notification HTML
function generateNotificationHtml(notification) {
const timeAgo = formatTimeAgo(new Date(notification.timestamp));
const unreadClass = notification.read ? "" : "unread";
return `
<div class="notification-item ${unreadClass}" data-notification-id="${notification.id}">
<div class="notification-title">${notification.title}</div>
<div class="notification-message">${notification.message}</div>
<div class="notification-time">${timeAgo}</div>
</div>
`;
}
// Helper function to format time ago
function formatTimeAgo(date) {
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
}
// Auto-generate random notifications every 30 seconds
setInterval(() => {
const randomUser = users[Math.floor(Math.random() * users.length)];
const notificationTypes = [
{
title: "New Order",
message: "You have received a new order",
type: "success",
},
{
title: "System Update",
message: "System has been updated",
type: "info",
},
{
title: "Payment Received",
message: "Payment has been processed",
type: "success",
},
{
title: "Server Alert",
message: "Server is experiencing high load",
type: "warning",
},
];
const randomNotification =
notificationTypes[Math.floor(Math.random() * notificationTypes.length)];
const notification = {
id: Date.now() + Math.random(),
title: randomNotification.title,
message: randomNotification.message,
type: randomNotification.type,
read: false,
timestamp: new Date().toISOString(),
};
// Add to user's notifications
const userNotifications = notifications.get(randomUser.id) || [];
userNotifications.unshift(notification);
notifications.set(randomUser.id, userNotifications);
// Find user's connection and send notification
const userConnection = wsx
.getConnections()
.find((conn) => conn.sessionData?.user?.id === randomUser.id);
if (userConnection) {
// Send toast notification
const toastHtml = `
<div class="toast toast-${notification.type}">
<div><strong>${notification.title}</strong><br>${notification.message}</div>
<button class="toast-close">×</button>
</div>
`;
wsx.sendToConnection(
userConnection.id,
"#toast-container",
toastHtml,
"beforeend"
);
// Update notification list
const notificationHtml = generateNotificationHtml(notification);
wsx.sendToConnection(
userConnection.id,
"#notification-list",
notificationHtml,
"afterbegin"
);
// Update badge
const unreadCount = userNotifications.filter((n) => !n.read).length;
wsx.sendToConnection(
userConnection.id,
"#notification-badge",
`<span class="notification-badge">${unreadCount}</span>`,
"outerHTML"
);
}
}, 30000);
app.use(express.static("public"));
app.listen(3000, () => {
console.log("Notifications server running on http://localhost:3000");
});
// Push notification service
class PushNotificationService {
constructor() {
this.subscriptions = new Map();
}
subscribe(userId, subscription) {
this.subscriptions.set(userId, subscription);
}
async sendPushNotification(userId, notification) {
const subscription = this.subscriptions.get(userId);
if (!subscription) return;
try {
await webpush.sendNotification(
subscription,
JSON.stringify({
title: notification.title,
body: notification.message,
icon: "/icon-192x192.png",
badge: "/badge-72x72.png",
actions: [
{ action: "open", title: "Open" },
{ action: "close", title: "Close" },
],
})
);
} catch (error) {
console.error("Push notification failed:", error);
}
}
}
const pushService = new PushNotificationService();
// Subscribe to push notifications
wsx.on("subscribe-push", async (request, connection) => {
const { subscription } = request.data;
const user = connection.sessionData?.user;
if (user && subscription) {
pushService.subscribe(user.id, subscription);
return {
id: request.id,
target: request.target,
html: `<div class="success">Push notifications enabled</div>`,
};
}
return {
id: request.id,
target: request.target,
html: `<div class="error">Failed to enable push notifications</div>`,
};
});
// Notification preferences
const userPreferences = new Map();
wsx.on("update-notification-preferences", async (request, connection) => {
const user = connection.sessionData?.user;
const { emailNotifications, pushNotifications, smsNotifications, types } =
request.data;
if (!user) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Please log in to update preferences</div>`,
};
}
userPreferences.set(user.id, {
email: emailNotifications,
push: pushNotifications,
sms: smsNotifications,
types: types || [],
});
return {
id: request.id,
target: request.target,
html: `<div class="success">Notification preferences updated</div>`,
};
});
// Check if user should receive notification
function shouldReceiveNotification(userId, notificationType) {
const preferences = userPreferences.get(userId);
if (!preferences) return true; // Default to sending
return preferences.types.includes(notificationType);
}
import nodemailer from "nodemailer";
// Email service
const emailTransporter = nodemailer.createTransporter({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
async function sendEmailNotification(user, notification) {
const preferences = userPreferences.get(user.id);
if (!preferences?.email) return;
try {
await emailTransporter.sendMail({
from: process.env.FROM_EMAIL,
to: user.email,
subject: notification.title,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>${notification.title}</h2>
<p>${notification.message}</p>
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee;">
<small style="color: #666;">
You received this email because you have email notifications enabled.
<a href="${process.env.APP_URL}/settings">Update your preferences</a>
</small>
</div>
</div>
`,
});
} catch (error) {
console.error("Email notification failed:", error);
}
}
// Notification templates
const notificationTemplates = {
welcome: {
title: "Welcome to {{appName}}!",
message: "Thank you for joining us, {{userName}}!",
type: "success",
},
orderConfirmation: {
title: "Order Confirmed",
message: "Your order #{{orderNumber}} has been confirmed",
type: "success",
},
paymentReceived: {
title: "Payment Received",
message: "We have received your payment of ${{amount}}",
type: "success",
},
systemMaintenance: {
title: "Scheduled Maintenance",
message: "System maintenance is scheduled for {{date}} at {{time}}",
type: "warning",
},
securityAlert: {
title: "Security Alert",
message: "Login detected from {{location}} at {{time}}",
type: "warning",
},
};
// Send templated notification
wsx.on("send-templated-notification", async (request, connection) => {
const { templateId, variables, targetUserId } = request.data;
const template = notificationTemplates[templateId];
if (!template) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Template not found</div>`,
};
}
// Replace template variables
let title = template.title;
let message = template.message;
Object.entries(variables).forEach(([key, value]) => {
title = title.replace(`{{${key}}}`, value);
message = message.replace(`{{${key}}}`, value);
});
const notification = {
id: Date.now() + Math.random(),
title,
message,
type: template.type,
read: false,
timestamp: new Date().toISOString(),
};
// Send to specific user or all users
const targetUsers = targetUserId
? [users.find((u) => u.id === targetUserId)]
: users;
targetUsers.forEach((user) => {
if (!user) return;
// Add to user's notifications
const userNotifications = notifications.get(user.id) || [];
userNotifications.unshift(notification);
notifications.set(user.id, userNotifications);
// Send to connected user
const userConnection = wsx
.getConnections()
.find((conn) => conn.sessionData?.user?.id === user.id);
if (userConnection) {
const toastHtml = `
<div class="toast toast-${notification.type}">
<div><strong>${notification.title}</strong><br>${notification.message}</div>
<button class="toast-close">×</button>
</div>
`;
wsx.sendToConnection(
userConnection.id,
"#toast-container",
toastHtml,
"beforeend"
);
}
});
return {
id: request.id,
target: request.target,
html: `<div class="success">Notification sent using template: ${templateId}</div>`,
};
});
// Track notification analytics
const notificationAnalytics = {
sent: new Map(),
opened: new Map(),
clicked: new Map(),
};
wsx.on("track-notification-event", async (request, connection) => {
const { notificationId, event, data } = request.data;
switch (event) {
case "sent":
notificationAnalytics.sent.set(notificationId, {
timestamp: Date.now(),
userId: connection.sessionData?.user?.id,
});
break;
case "opened":
notificationAnalytics.opened.set(notificationId, {
timestamp: Date.now(),
userId: connection.sessionData?.user?.id,
});
break;
case "clicked":
notificationAnalytics.clicked.set(notificationId, {
timestamp: Date.now(),
userId: connection.sessionData?.user?.id,
action: data.action,
});
break;
}
return {
id: request.id,
target: request.target,
html: "", // No visual feedback needed
};
});
// Get notification analytics
wsx.on("get-notification-analytics", async (request, connection) => {
const user = connection.sessionData?.user;
if (!user?.isAdmin) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Admin access required</div>`,
};
}
const totalSent = notificationAnalytics.sent.size;
const totalOpened = notificationAnalytics.opened.size;
const totalClicked = notificationAnalytics.clicked.size;
const openRate =
totalSent > 0 ? ((totalOpened / totalSent) * 100).toFixed(1) : 0;
const clickRate =
totalOpened > 0 ? ((totalClicked / totalOpened) * 100).toFixed(1) : 0;
const analyticsHtml = `
<div class="notification-analytics">
<h3>Notification Analytics</h3>
<div class="stats-grid">
<div class="stat">
<div class="stat-value">${totalSent}</div>
<div class="stat-label">Total Sent</div>
</div>
<div class="stat">
<div class="stat-value">${totalOpened}</div>
<div class="stat-label">Opened</div>
</div>
<div class="stat">
<div class="stat-value">${totalClicked}</div>
<div class="stat-label">Clicked</div>
</div>
<div class="stat">
<div class="stat-value">${openRate}%</div>
<div class="stat-label">Open Rate</div>
</div>
<div class="stat">
<div class="stat-value">${clickRate}%</div>
<div class="stat-label">Click Rate</div>
</div>
</div>
</div>
`;
return {
id: request.id,
target: request.target,
html: analyticsHtml,
};
});