diff --git a/Server-Side Components/Script Includes/Email Template Debugger/README.md b/Server-Side Components/Script Includes/Email Template Debugger/README.md new file mode 100644 index 0000000000..20ae879771 --- /dev/null +++ b/Server-Side Components/Script Includes/Email Template Debugger/README.md @@ -0,0 +1,143 @@ +# Email Template Debugger + +## Overview +A powerful utility for ServiceNow developers to debug and preview email notifications in real-time. This tool helps visualize how email templates will render with different data contexts, test notification conditions, and troubleshoot email-related issues without sending actual emails. + +## Features +- Live preview of email templates +- Variable substitution testing +- HTML/Plain text toggle view +- Attachment validation +- Template syntax checking +- Recipient list validation +- Condition script testing +- Email script debugging +- Performance metrics + +## Requirements +- ServiceNow instance with admin access +- Notification management rights +- Script Include access +- Email administration rights + +## Implementation Steps +1. Create a new Script Include using script.js +2. Set up the debugging page using debugger_page.js +3. Configure access controls +4. Import any required style sheets +5. Test with sample notifications + +## Components + +### Script Include +- Handles template processing +- Manages variable substitution +- Validates email scripts +- Processes attachments +- Checks recipient lists + +### Debugging Interface +- Template preview panel +- Variable input section +- Script testing area +- Results display +- Error highlighting + +## Usage Example +```javascript +var emailDebugger = new EmailTemplateDebugger(); +var result = emailDebugger.debugTemplate({ + notificationId: 'sys_id_of_notification', + testRecord: 'sys_id_of_test_record', + recipientList: ['user1@example.com'], + variables: { + 'incident.number': 'INC0010001', + 'incident.short_description': 'Test incident' + } +}); +``` + +## Features in Detail + +### Template Analysis +- Syntax validation +- Missing variable detection +- Script error identification +- HTML structure verification +- CSS compatibility check + +### Performance Monitoring +- Template processing time +- Script execution metrics +- Database query impact +- Attachment processing time +- Overall generation time + +### Security Checks +- Recipient validation +- Domain verification +- Script injection prevention +- Attachment size validation +- Permission verification + +### Debugging Tools +- Step-by-step template processing +- Variable resolution tracking +- Script execution logging +- Error stack traces +- Query optimization hints + +## Best Practices +1. Always test with sample data first +2. Verify all variable substitutions +3. Check both HTML and plain text versions +4. Validate attachment handling +5. Test with different record types +6. Monitor performance metrics +7. Review security implications + +## Error Handling +The debugger provides detailed error information for: +- Syntax errors in templates +- Missing or invalid variables +- Script execution failures +- Recipient list issues +- Attachment problems +- Permission errors + +## Performance Considerations +- Cache frequently used templates +- Optimize script execution +- Batch process attachments +- Minimize database queries +- Use efficient variable substitution + +## Security Notes +- Validate all input data +- Check recipient permissions +- Sanitize variable content +- Verify attachment types +- Monitor script execution + +## Troubleshooting +Common issues and solutions: +1. Template not found + - Verify notification sys_id + - Check access permissions +2. Variable substitution fails + - Confirm variable names + - Check data types +3. Script errors + - Review script syntax + - Check variable scope +4. Attachment issues + - Verify file permissions + - Check size limits + +## Extensions +The debugger can be extended with: +- Custom validation rules +- Additional preview formats +- New debugging tools +- Performance analyzers +- Security checkers \ No newline at end of file diff --git a/Server-Side Components/Script Includes/Email Template Debugger/debugger_page.js b/Server-Side Components/Script Includes/Email Template Debugger/debugger_page.js new file mode 100644 index 0000000000..2756d5e4aa --- /dev/null +++ b/Server-Side Components/Script Includes/Email Template Debugger/debugger_page.js @@ -0,0 +1,349 @@ +// Client-side debugger interface +var g_emailDebugger = Class.create({ + initialize: function() { + this.debuggerUI = this._createDebuggerUI(); + this.currentTemplate = null; + this._attachEventHandlers(); + }, + + _createDebuggerUI: function() { + var container = new Element('div', { + 'class': 'email-debugger-container' + }); + + // Create header + var header = new Element('div', { + 'class': 'debugger-header' + }); + header.insert(new Element('h2').update('Email Template Debugger')); + container.insert(header); + + // Create main content area + var content = new Element('div', { + 'class': 'debugger-content' + }); + + // Template selector + content.insert(this._createTemplateSelector()); + + // Test data section + content.insert(this._createTestDataSection()); + + // Preview section + content.insert(this._createPreviewSection()); + + // Debug log section + content.insert(this._createDebugSection()); + + container.insert(content); + + // Add styles + this._addStyles(); + + return container; + }, + + _createTemplateSelector: function() { + var section = new Element('div', { + 'class': 'debugger-section' + }); + + section.insert(new Element('h3').update('Select Template')); + + var selector = new Element('select', { + 'class': 'template-selector' + }); + this._loadTemplates(selector); + + section.insert(selector); + return section; + }, + + _createTestDataSection: function() { + var section = new Element('div', { + 'class': 'debugger-section' + }); + + section.insert(new Element('h3').update('Test Data')); + + // Record selector + var recordInput = new Element('input', { + 'type': 'text', + 'placeholder': 'Record sys_id', + 'class': 'test-record-input' + }); + section.insert(recordInput); + + // Variables editor + var variablesEditor = new Element('textarea', { + 'class': 'variables-editor', + 'placeholder': 'Enter test variables in JSON format' + }); + section.insert(variablesEditor); + + // Test button + var testButton = new Element('button', { + 'class': 'test-button' + }).update('Test Template'); + section.insert(testButton); + + return section; + }, + + _createPreviewSection: function() { + var section = new Element('div', { + 'class': 'debugger-section preview-section' + }); + + section.insert(new Element('h3').update('Preview')); + + // View toggles + var toggles = new Element('div', { + 'class': 'view-toggles' + }); + toggles.insert(new Element('button', { + 'class': 'toggle-html active' + }).update('HTML')); + toggles.insert(new Element('button', { + 'class': 'toggle-plain' + }).update('Plain Text')); + section.insert(toggles); + + // Preview iframe + var preview = new Element('iframe', { + 'class': 'preview-frame' + }); + section.insert(preview); + + return section; + }, + + _createDebugSection: function() { + var section = new Element('div', { + 'class': 'debugger-section debug-section' + }); + + section.insert(new Element('h3').update('Debug Log')); + + var log = new Element('div', { + 'class': 'debug-log' + }); + section.insert(log); + + return section; + }, + + _loadTemplates: function(selector) { + // Load available email templates + var ga = new GlideAjax('EmailTemplateDebugger'); + ga.addParam('sysparm_name', 'getTemplateList'); + ga.getXMLAnswer(function(answer) { + var templates = JSON.parse(answer); + templates.forEach(function(template) { + selector.insert(new Element('option', { + 'value': template.sys_id + }).update(template.name)); + }); + }); + }, + + _attachEventHandlers: function() { + var self = this; + + // Template selection + this.debuggerUI.down('.template-selector').observe('change', function(e) { + self._loadTemplate(e.target.value); + }); + + // Test button + this.debuggerUI.down('.test-button').observe('click', function() { + self._runTest(); + }); + + // View toggles + this.debuggerUI.down('.view-toggles').observe('click', function(e) { + if (e.target.hasClassName('toggle-html')) { + self._showHtmlView(); + } else if (e.target.hasClassName('toggle-plain')) { + self._showPlainView(); + } + }); + }, + + _loadTemplate: function(templateId) { + var ga = new GlideAjax('EmailTemplateDebugger'); + ga.addParam('sysparm_name', 'debugTemplate'); + ga.addParam('sysparm_template_id', templateId); + ga.getXMLAnswer(this._updatePreview.bind(this)); + }, + + _runTest: function() { + var testData = { + record: this.debuggerUI.down('.test-record-input').value, + variables: this._parseVariables() + }; + + var ga = new GlideAjax('EmailTemplateDebugger'); + ga.addParam('sysparm_name', 'debugTemplate'); + ga.addParam('sysparm_test_data', JSON.stringify(testData)); + ga.getXMLAnswer(this._updatePreview.bind(this)); + }, + + _parseVariables: function() { + try { + return JSON.parse(this.debuggerUI.down('.variables-editor').value); + } catch (e) { + this._logError('Invalid variables JSON: ' + e.message); + return {}; + } + }, + + _updatePreview: function(response) { + var result = JSON.parse(response); + + if (result.status === 'success') { + this._updatePreviewContent(result.data.preview); + this._updateDebugLog(result.data.debug); + this._updateMetrics(result.data.metrics); + } else { + this._logError(result.message); + } + }, + + _updatePreviewContent: function(preview) { + var frame = this.debuggerUI.down('.preview-frame'); + var doc = frame.contentDocument || frame.contentWindow.document; + doc.open(); + doc.write(preview.html); + doc.close(); + }, + + _updateDebugLog: function(log) { + var logContainer = this.debuggerUI.down('.debug-log'); + logContainer.update(''); + + log.forEach(function(entry) { + var logEntry = new Element('div', { + 'class': 'log-entry' + }); + logEntry.insert(new Element('span', { + 'class': 'timestamp' + }).update('[' + entry.timestamp + 'ms] ')); + logEntry.insert(new Element('span', { + 'class': 'message' + }).update(entry.message)); + + if (entry.data) { + logEntry.insert(new Element('pre', { + 'class': 'data' + }).update(JSON.stringify(entry.data, null, 2))); + } + + logContainer.insert(logEntry); + }); + }, + + _updateMetrics: function(metrics) { + var metricsHtml = '
'; + Object.keys(metrics).forEach(function(key) { + metricsHtml += '
' + + '' + key + ': ' + + '' + metrics[key] + 'ms' + + '
'; + }); + metricsHtml += '
'; + + this.debuggerUI.down('.debug-section').insert({ + top: new Element('div').update(metricsHtml) + }); + }, + + _logError: function(message) { + var logContainer = this.debuggerUI.down('.debug-log'); + logContainer.insert({ + top: new Element('div', { + 'class': 'error-entry' + }).update(message) + }); + }, + + _addStyles: function() { + var styles = ` + .email-debugger-container { + padding: 20px; + background: #f5f5f5; + border-radius: 4px; + font-family: 'Helvetica Neue', Arial, sans-serif; + } + + .debugger-section { + margin-bottom: 20px; + padding: 15px; + background: white; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + + .preview-frame { + width: 100%; + height: 500px; + border: 1px solid #ddd; + border-radius: 4px; + } + + .debug-log { + max-height: 300px; + overflow-y: auto; + font-family: monospace; + background: #2b2b2b; + color: #e6e6e6; + padding: 10px; + border-radius: 4px; + } + + .log-entry { + margin-bottom: 5px; + line-height: 1.4; + } + + .error-entry { + color: #ff6b6b; + font-weight: bold; + } + + .view-toggles button { + margin-right: 10px; + padding: 5px 15px; + border: 1px solid #ddd; + border-radius: 4px; + background: white; + cursor: pointer; + } + + .view-toggles button.active { + background: #007bff; + color: white; + border-color: #0056b3; + } + + .metrics-summary { + margin-bottom: 15px; + padding: 10px; + background: #e9ecef; + border-radius: 4px; + } + + .metric { + display: inline-block; + margin-right: 20px; + } + + .metric-name { + font-weight: bold; + } + `; + + var styleSheet = new Element('style').update(styles); + $$('head')[0].insert(styleSheet); + } +}); \ No newline at end of file diff --git a/Server-Side Components/Script Includes/Email Template Debugger/script.js b/Server-Side Components/Script Includes/Email Template Debugger/script.js new file mode 100644 index 0000000000..7b780d82ee --- /dev/null +++ b/Server-Side Components/Script Includes/Email Template Debugger/script.js @@ -0,0 +1,228 @@ +var EmailTemplateDebugger = Class.create(); +EmailTemplateDebugger.prototype = Object.extendsObject(AbstractAjaxProcessor, { + initialize: function() { + this.resultCache = {}; + this.debugLog = []; + this.startTime = 0; + this.metrics = {}; + }, + + debugTemplate: function(params) { + try { + this.startDebugSession(); + + // Validate input parameters + if (!this._validateParams(params)) { + throw new Error('Invalid parameters provided'); + } + + // Get notification template + var template = this._getNotificationTemplate(params.notificationId); + this.logDebug('Template retrieved', template); + + // Process variables + var processedTemplate = this._processVariables(template, params.variables); + this.logDebug('Variables processed', processedTemplate); + + // Validate recipients + this._validateRecipients(params.recipientList); + this.logDebug('Recipients validated', params.recipientList); + + // Check conditions + if (!this._evaluateConditions(template, params.testRecord)) { + return this._formatResult('Notification conditions not met', 'warning'); + } + + // Process attachments + var attachments = this._processAttachments(template, params.testRecord); + this.logDebug('Attachments processed', attachments); + + // Generate preview + var preview = this._generatePreview(processedTemplate); + this.logDebug('Preview generated', preview); + + return this._formatResult('Success', 'success', { + preview: preview, + metrics: this.getMetrics(), + debug: this.getDebugLog() + }); + + } catch (e) { + return this._formatResult(e.message, 'error', { + stack: e.stack, + debug: this.getDebugLog() + }); + } + }, + + _validateParams: function(params) { + if (!params || !params.notificationId) { + return false; + } + return true; + }, + + _getNotificationTemplate: function(notificationId) { + var startTime = new Date().getTime(); + + var notification = new GlideRecord('sysevent_email_template'); + if (!notification.get(notificationId)) { + throw new Error('Template not found: ' + notificationId); + } + + this._addMetric('templateRetrieval', new Date().getTime() - startTime); + + return { + subject: notification.getValue('subject'), + body: notification.getValue('message_html'), + plainText: notification.getValue('message'), + conditions: notification.getValue('condition') + }; + }, + + _processVariables: function(template, variables) { + var startTime = new Date().getTime(); + + var processed = { + subject: template.subject, + body: template.body, + plainText: template.plainText + }; + + // Process each variable + Object.keys(variables || {}).forEach(function(key) { + var regex = new RegExp('\\$\\{' + key + '\\}', 'g'); + processed.subject = processed.subject.replace(regex, variables[key]); + processed.body = processed.body.replace(regex, variables[key]); + processed.plainText = processed.plainText.replace(regex, variables[key]); + }); + + this._addMetric('variableProcessing', new Date().getTime() - startTime); + + return processed; + }, + + _validateRecipients: function(recipients) { + var startTime = new Date().getTime(); + + if (!recipients || !recipients.length) { + throw new Error('No recipients specified'); + } + + recipients.forEach(function(email) { + if (!this._isValidEmail(email)) { + throw new Error('Invalid email format: ' + email); + } + }, this); + + this._addMetric('recipientValidation', new Date().getTime() - startTime); + }, + + _isValidEmail: function(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + }, + + _evaluateConditions: function(template, testRecord) { + var startTime = new Date().getTime(); + + if (!template.conditions) { + return true; + } + + try { + var condition = template.conditions; + var gr = new GlideRecord(testRecord.table); + gr.get(testRecord.sys_id); + + var evaluator = new GlideRecordConditionEvaluator(); + var result = evaluator.evaluateCondition(gr, condition); + + this._addMetric('conditionEvaluation', new Date().getTime() - startTime); + + return result; + } catch (e) { + this.logDebug('Condition evaluation error', e.message); + return false; + } + }, + + _processAttachments: function(template, testRecord) { + var startTime = new Date().getTime(); + + var attachments = []; + var gr = new GlideRecord('sys_attachment'); + gr.addQuery('table_sys_id', testRecord.sys_id); + gr.query(); + + while (gr.next()) { + attachments.push({ + name: gr.getValue('file_name'), + size: gr.getValue('size_bytes'), + type: gr.getValue('content_type') + }); + } + + this._addMetric('attachmentProcessing', new Date().getTime() - startTime); + + return attachments; + }, + + _generatePreview: function(processedTemplate) { + var startTime = new Date().getTime(); + + var preview = { + html: this._sanitizeHTML(processedTemplate.body), + plain: processedTemplate.plainText, + subject: processedTemplate.subject + }; + + this._addMetric('previewGeneration', new Date().getTime() - startTime); + + return preview; + }, + + _sanitizeHTML: function(html) { + // Basic HTML sanitization + return html.replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/on\w+="[^"]*"/g, ''); + }, + + startDebugSession: function() { + this.startTime = new Date().getTime(); + this.debugLog = []; + this.metrics = {}; + }, + + logDebug: function(message, data) { + this.debugLog.push({ + timestamp: new Date().getTime() - this.startTime, + message: message, + data: data + }); + }, + + _addMetric: function(name, duration) { + this.metrics[name] = duration; + }, + + getMetrics: function() { + var totalTime = new Date().getTime() - this.startTime; + this.metrics.total = totalTime; + return this.metrics; + }, + + getDebugLog: function() { + return this.debugLog; + }, + + _formatResult: function(message, status, data) { + return { + message: message, + status: status, + timestamp: new Date().getTime(), + data: data || {} + }; + }, + + type: 'EmailTemplateDebugger' +}); \ No newline at end of file