Created
April 8, 2026 17:32
-
-
Save assada/3b7c44b42b25db01556b4071a4783b7b to your computer and use it in GitHub Desktop.
TIG Rule Engine Tester
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TIG Rule Engine Tester</title> | |
| <script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/javascript/javascript.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/yaml/yaml.min.js"></script> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #1a1a1a; height: 100vh; display: flex; flex-direction: column; } | |
| header { background: #fff; border-bottom: 1px solid #ddd; padding: 10px 20px; display: flex; align-items: center; gap: 16px; flex-shrink: 0; } | |
| header h1 { font-size: 16px; font-weight: 600; } | |
| header button { padding: 6px 14px; font-size: 13px; border: 1px solid #ccc; border-radius: 4px; background: #fff; cursor: pointer; } | |
| header button:hover { background: #f0f0f0; } | |
| header button.primary { background: #2563eb; color: #fff; border-color: #2563eb; } | |
| header button.primary:hover { background: #1d4ed8; } | |
| select.preset { padding: 5px 8px; font-size: 12px; border: 1px solid #ccc; border-radius: 4px; background: #fff; max-width: 200px; } | |
| .main { display: flex; flex: 1; overflow: hidden; } | |
| .col { display: flex; flex-direction: column; border-right: 1px solid #ddd; overflow: hidden; } | |
| .col:last-child { border-right: none; } | |
| .col-input { flex: 1; min-width: 0; } | |
| .col-rules { flex: 1; min-width: 0; } | |
| .col-results { flex: 1.2; min-width: 0; background: #fff; } | |
| .col-header { padding: 8px 12px; background: #fafafa; border-bottom: 1px solid #ddd; font-size: 13px; font-weight: 600; color: #555; flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; } | |
| .col-header .badge { font-size: 11px; background: #e5e7eb; padding: 2px 8px; border-radius: 10px; font-weight: 500; } | |
| textarea { flex: 1; width: 100%; border: none; outline: none; resize: none; padding: 12px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 12.5px; line-height: 1.5; background: #fff; tab-size: 2; } | |
| textarea:focus { background: #fefefe; } | |
| .CodeMirror { flex: 1; height: auto !important; font-size: 12.5px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; border: none; } | |
| .cm-wrap { flex: 1; display: flex; flex-direction: column; overflow: hidden; } | |
| .results-body { flex: 1; overflow-y: auto; padding: 12px; } | |
| .stats-bar { display: flex; gap: 12px; font-size: 12px; color: #666; padding: 8px 12px; background: #fafafa; border-top: 1px solid #ddd; flex-shrink: 0; } | |
| .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } | |
| .dot-green { background: #22c55e; } | |
| .dot-yellow { background: #f59e0b; } | |
| .dot-gray { background: #d1d5db; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>TIG Rule Engine Tester</h1> | |
| <button class="primary" id="btn-eval">Evaluate (Ctrl+Enter)</button> | |
| <select class="preset" id="preset-select"> | |
| <option value="">-- Event Presets --</option> | |
| <option value="editor_web_rich">1. Editor open (Web) rich attrs -- fanout amp+dwh, dedup</option> | |
| <option value="editor_android">2. Editor save (Android) -- cross: amp-prod + amp-android + dwh</option> | |
| <option value="ios_document_signed">3. Document signed (iOS) -- amp-ios + amp-prod + dwh</option> | |
| <option value="legacy_upload">4. Legacy: upload_document -- DWH only, minimal event</option> | |
| <option value="legacy_send_to_sign">5. Legacy: send_to_sign -- DWH with many dbParams</option> | |
| <option value="amplitude_with_dbparams">6. Amplitude track() with dbParams -- cross-route test</option> | |
| <option value="backend_qless">7. Backend Qless event -- sn_api_*, low priority filter test</option> | |
| <option value="guest_no_userid">8. Guest user (no user_id) -- edge case, device_id only</option> | |
| </select> | |
| <button id="btn-clear">Clear</button> | |
| </header> | |
| <div class="main"> | |
| <div class="col col-input"> | |
| <div class="col-header">Canonical Event (JSON)<span class="badge" id="input-status">waiting</span></div> | |
| <textarea id="input" placeholder='Paste canonical event JSON here...'></textarea> | |
| </div> | |
| <div class="col col-rules"> | |
| <div class="col-header">Routing Config (YAML)<span class="badge" id="rules-status">waiting</span></div> | |
| <textarea id="rules" placeholder="Paste routing config JSON here..."></textarea> | |
| </div> | |
| <div class="col col-results"> | |
| <div class="col-header">Evaluation Results<span class="badge" id="result-count">-</span></div> | |
| <div class="results-body" id="results"></div> | |
| <div class="stats-bar" id="stats-bar" style="display:none"> | |
| <span><span class="dot dot-green"></span> <span id="stat-send">0</span> destinations</span> | |
| <span><span class="dot dot-yellow"></span> <span id="stat-filtered">0</span> priority-filtered</span> | |
| <span><span class="dot dot-gray"></span> <span id="stat-rules-matched">0</span>/<span id="stat-rules-total">0</span> rules matched</span> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // =========== RULE ENGINE (exact TIG-05 spec) =========== | |
| function resolveField(obj, path) { | |
| const parts = path.split('.'); | |
| let val = obj; | |
| for (const p of parts) { | |
| if (val == null || typeof val !== 'object') return undefined; | |
| val = val[p]; | |
| } | |
| return val; | |
| } | |
| function checkCondition(event, cond) { | |
| const val = cond.by === 'event_name' ? event.event_name : resolveField(event, cond.field || ''); | |
| const fieldLabel = cond.by === 'field' ? cond.field : 'event_name'; | |
| if (val === undefined || val === null) return { matched: false, reason: fieldLabel + ' is null/undefined' }; | |
| const strVal = String(val); | |
| if (cond.type === 'regex') { | |
| try { | |
| const m = new RegExp(cond.pattern).test(strVal); | |
| return { matched: m, reason: '"' + strVal + '" ' + (m ? 'matches' : 'does not match') + ' /' + cond.pattern + '/' }; | |
| } catch (e) { return { matched: false, reason: 'invalid regex: ' + e.message }; } | |
| } | |
| if (cond.type === 'match') { | |
| const expected = String(cond.value); | |
| const m = strVal === expected; | |
| return { matched: m, reason: '"' + strVal + '" ' + (m ? '===' : '!==') + ' "' + expected + '"' }; | |
| } | |
| return { matched: false, reason: 'unknown type: ' + cond.type }; | |
| } | |
| function evaluateRules(event, config) { | |
| const destConfigs = config.config || {}; | |
| const rules = config.rules || {}; | |
| const destinations = {}; | |
| const ruleTraces = []; | |
| for (const [name, rule] of Object.entries(rules)) { | |
| const conditions = rule.conditions || []; | |
| const condResults = conditions.map(function(c) { | |
| var r = checkCondition(event, c); | |
| r.condition = c; | |
| return r; | |
| }); | |
| const allMatched = condResults.every(function(c) { return c.matched; }); | |
| ruleTraces.push({ name: name, priority: rule.priority || 0, matched: allMatched, conditions: condResults, destinations: rule.destinations || [] }); | |
| if (allMatched) { | |
| (rule.destinations || []).forEach(function(dest) { | |
| const target = dest.target; | |
| const dc = destConfigs[target] || {}; | |
| const minP = dc.minimum_priority || 0; | |
| const ruleP = rule.priority || 0; | |
| if (!destinations[target]) destinations[target] = { maxPriority: 0, matchedRules: [], sent: false, filteredBy: null, config: dc }; | |
| var d = destinations[target]; | |
| d.matchedRules.push({ rule: name, priority: ruleP }); | |
| if (ruleP >= minP) { d.sent = true; if (ruleP > d.maxPriority) d.maxPriority = ruleP; } | |
| else if (!d.sent) { d.filteredBy = { rule: name, rulePriority: ruleP, minPriority: minP }; } | |
| }); | |
| } | |
| } | |
| // Build output payloads | |
| for (const [target, d] of Object.entries(destinations)) { | |
| d.outputPayload = buildOutput(event, d.config); | |
| } | |
| return { destinations: destinations, ruleTraces: ruleTraces }; | |
| } | |
| function buildOutput(event, dc) { | |
| var out = { event_name: event.event_name, event_properties: Object.assign({}, event.event_properties || {}) }; | |
| var allowed = dc.allowedUserProps; | |
| var up = event.user_properties || {}; | |
| if (allowed && Array.isArray(allowed)) { | |
| var filtered = {}; | |
| allowed.forEach(function(k) { if (up[k] !== undefined) filtered[k] = up[k]; }); | |
| out.user_properties = filtered; | |
| out._filtered = true; | |
| out._original = Object.keys(up).length; | |
| out._kept = Object.keys(filtered).length; | |
| } else { | |
| out.user_properties = Object.assign({}, up); | |
| out._filtered = false; | |
| } | |
| if (event.identity) out.identity = Object.assign({}, event.identity); | |
| if (event.source) out.source = Object.assign({}, event.source); | |
| return out; | |
| } | |
| // =========== RENDER (safe, no innerHTML with user data) =========== | |
| function clearElement(el) { while (el.firstChild) el.removeChild(el.firstChild); } | |
| function el(tag, attrs, children) { | |
| var e = document.createElement(tag); | |
| if (attrs) for (var k in attrs) { | |
| if (k === 'style' && typeof attrs[k] === 'object') Object.assign(e.style, attrs[k]); | |
| else if (k === 'textContent') e.textContent = attrs[k]; | |
| else if (k === 'className') e.className = attrs[k]; | |
| else e.setAttribute(k, attrs[k]); | |
| } | |
| if (children) children.forEach(function(c) { if (typeof c === 'string') e.appendChild(document.createTextNode(c)); else if (c) e.appendChild(c); }); | |
| return e; | |
| } | |
| function renderResults(results, event) { | |
| var container = document.getElementById('results'); | |
| clearElement(container); | |
| var allDests = Object.keys(results.destinations); | |
| var sentDests = allDests.filter(function(d) { return results.destinations[d].sent; }); | |
| var filteredDests = allDests.filter(function(d) { return !results.destinations[d].sent; }); | |
| var matchedRules = results.ruleTraces.filter(function(r) { return r.matched; }).length; | |
| // Destinations section | |
| var destSection = el('div', { className: 'result-section' }, [el('h3', { textContent: 'Destinations', style: { fontSize: '12px', fontWeight: '600', textTransform: 'uppercase', color: '#888', marginBottom: '6px', letterSpacing: '0.5px' } })]); | |
| if (sentDests.length === 0 && filteredDests.length === 0) { | |
| destSection.appendChild(el('div', { style: { background: '#f9fafb', border: '1px solid #e5e7eb', borderRadius: '6px', padding: '10px 12px', opacity: '0.6' } }, [el('b', { textContent: 'No rules matched' }), document.createTextNode(' \u2014 default fallback applies')])); | |
| } | |
| sentDests.forEach(function(target) { | |
| var d = results.destinations[target]; | |
| var card = el('div', { style: { background: '#f0fdf4', border: '1px solid #22c55e', borderRadius: '6px', padding: '10px 12px', marginBottom: '8px' } }); | |
| var nameRow = el('div', { style: { fontWeight: '600', fontSize: '13px' } }, [ | |
| document.createTextNode(target + ' '), | |
| el('span', { textContent: 'SEND', style: { fontSize: '10px', padding: '1px 6px', borderRadius: '3px', background: '#dcfce7', color: '#166534' } }), | |
| el('span', { textContent: ' priority ' + d.maxPriority, style: { fontSize: '11px', color: '#888' } }) | |
| ]); | |
| card.appendChild(nameRow); | |
| var matchedBy = el('div', { style: { fontSize: '12px', color: '#666', marginTop: '4px' } }, [document.createTextNode('Matched by: ')]); | |
| d.matchedRules.forEach(function(r, i) { | |
| if (i > 0) matchedBy.appendChild(document.createTextNode(', ')); | |
| matchedBy.appendChild(el('b', { textContent: r.rule })); | |
| matchedBy.appendChild(document.createTextNode('(p' + r.priority + ')')); | |
| }); | |
| card.appendChild(matchedBy); | |
| // Output payload | |
| var p = d.outputPayload; | |
| var details = el('details', { style: { marginTop: '6px' } }); | |
| var summaryText = 'Output payload'; | |
| if (p._filtered) summaryText += ' (user_properties: ' + p._kept + '/' + p._original + ' after allowedUserProps filter)'; | |
| details.appendChild(el('summary', { textContent: summaryText, style: { fontSize: '11px', color: '#888', cursor: 'pointer' } })); | |
| var display = Object.assign({}, p); | |
| delete display._filtered; delete display._original; delete display._kept; | |
| details.appendChild(el('pre', { textContent: JSON.stringify(display, null, 2), style: { fontSize: '11px', background: '#fff', border: '1px solid #eee', borderRadius: '3px', padding: '6px 8px', marginTop: '4px', maxHeight: '200px', overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-all' } })); | |
| card.appendChild(details); | |
| destSection.appendChild(card); | |
| }); | |
| filteredDests.forEach(function(target) { | |
| var d = results.destinations[target]; | |
| var f = d.filteredBy; | |
| var card = el('div', { style: { background: '#fffbeb', border: '1px solid #f59e0b', borderRadius: '6px', padding: '10px 12px', marginBottom: '8px' } }); | |
| card.appendChild(el('div', { style: { fontWeight: '600', fontSize: '13px' } }, [ | |
| document.createTextNode(target + ' '), | |
| el('span', { textContent: 'PRIORITY FILTERED', style: { fontSize: '10px', padding: '1px 6px', borderRadius: '3px', background: '#fef3c7', color: '#92400e' } }) | |
| ])); | |
| card.appendChild(el('div', { textContent: 'Rule "' + f.rule + '" matched (priority ' + f.rulePriority + ') but destination requires minimum_priority ' + f.minPriority, style: { fontSize: '12px', color: '#666', marginTop: '4px' } })); | |
| destSection.appendChild(card); | |
| }); | |
| container.appendChild(destSection); | |
| // Rule traces section | |
| var traceSection = el('div', { className: 'result-section' }, [el('h3', { textContent: 'Rule Evaluation Trace', style: { fontSize: '12px', fontWeight: '600', textTransform: 'uppercase', color: '#888', marginBottom: '6px', letterSpacing: '0.5px' } })]); | |
| results.ruleTraces.forEach(function(trace) { | |
| var traceDiv = el('div', { style: { fontSize: '12px', marginTop: '4px', marginBottom: '8px' } }); | |
| var icon = trace.matched ? '\u2705' : '\u274c'; | |
| var color = trace.matched ? '#16a34a' : '#9ca3af'; | |
| var header = el('span', { style: { color: color } }, [document.createTextNode(icon + ' '), el('b', { textContent: trace.name }), document.createTextNode(' (priority ' + trace.priority + ')')]); | |
| traceDiv.appendChild(header); | |
| trace.conditions.forEach(function(c) { | |
| var cIcon = c.matched ? '\u2713' : '\u2717'; | |
| var cColor = c.matched ? '#16a34a' : '#dc2626'; | |
| var condLabel = c.condition.by === 'field' ? c.condition.field : 'event_name'; | |
| var line = el('div', { style: { marginLeft: '16px', color: cColor } }, [ | |
| document.createTextNode(cIcon + ' '), | |
| el('code', { textContent: c.condition.type }), | |
| document.createTextNode(' by '), | |
| el('code', { textContent: condLabel }), | |
| document.createTextNode(': ' + c.reason) | |
| ]); | |
| traceDiv.appendChild(line); | |
| }); | |
| if (trace.matched) { | |
| var destLine = el('div', { style: { marginLeft: '16px', color: '#555' } }, [document.createTextNode('\u2192 destinations: ')]); | |
| trace.destinations.forEach(function(d, i) { | |
| if (i > 0) destLine.appendChild(document.createTextNode(', ')); | |
| destLine.appendChild(el('b', { textContent: d.target })); | |
| }); | |
| traceDiv.appendChild(destLine); | |
| } | |
| traceSection.appendChild(traceDiv); | |
| }); | |
| container.appendChild(traceSection); | |
| // Stats bar | |
| document.getElementById('stats-bar').style.display = 'flex'; | |
| document.getElementById('stat-send').textContent = sentDests.length; | |
| document.getElementById('stat-filtered').textContent = filteredDests.length; | |
| document.getElementById('stat-rules-matched').textContent = matchedRules; | |
| document.getElementById('stat-rules-total').textContent = results.ruleTraces.length; | |
| document.getElementById('result-count').textContent = sentDests.length + ' dest'; | |
| } | |
| // =========== ACTIONS =========== | |
| function doEvaluate() { | |
| var inputEl = document.getElementById('input'); | |
| var rulesEl = document.getElementById('rules'); | |
| var resultsEl = document.getElementById('results'); | |
| var event, config; | |
| try { | |
| event = JSON.parse(inputEl.value); | |
| document.getElementById('input-status').textContent = 'valid'; | |
| document.getElementById('input-status').style.color = '#16a34a'; | |
| } catch (e) { | |
| clearElement(resultsEl); | |
| resultsEl.appendChild(el('div', { style: { background: '#fef2f2', border: '1px solid #fecaca', borderRadius: '6px', padding: '10px 12px', color: '#991b1b', fontSize: '12px' } }, [el('b', { textContent: 'Input JSON error: ' }), document.createTextNode(e.message)])); | |
| document.getElementById('input-status').textContent = 'error'; | |
| document.getElementById('input-status').style.color = '#dc2626'; | |
| return; | |
| } | |
| try { | |
| config = jsyaml.load(rulesEl.value); | |
| document.getElementById('rules-status').textContent = 'valid'; | |
| document.getElementById('rules-status').style.color = '#16a34a'; | |
| } catch (e) { | |
| clearElement(resultsEl); | |
| resultsEl.appendChild(el('div', { style: { background: '#fef2f2', border: '1px solid #fecaca', borderRadius: '6px', padding: '10px 12px', color: '#991b1b', fontSize: '12px' } }, [el('b', { textContent: 'YAML parse error: ' }), document.createTextNode(e.message)])); | |
| document.getElementById('rules-status').textContent = 'error'; | |
| document.getElementById('rules-status').style.color = '#dc2626'; | |
| return; | |
| } | |
| renderResults(evaluateRules(event, config), event); | |
| } | |
| // =========== SHARED CONFIG (YAML, covers all 8 presets) =========== | |
| var SHARED_CONFIG_YAML = [ | |
| 'config:', | |
| ' amplitude-prod:', | |
| ' minimum_priority: 50', | |
| ' amplitude-android:', | |
| ' minimum_priority: 50', | |
| ' amplitude-ios:', | |
| ' minimum_priority: 50', | |
| ' dwh:', | |
| ' minimum_priority: 0', | |
| ' allowedUserProps:', | |
| ' - subscription', | |
| ' - is_premium', | |
| ' - plan_name', | |
| '', | |
| 'rules:', | |
| ' # High-priority editor events to Amplitude + DWH', | |
| ' editor_to_prod_and_dwh:', | |
| ' priority: 80', | |
| ' conditions:', | |
| ' - type: regex', | |
| ' by: event_name', | |
| ' pattern: "sn_editor_(.+)"', | |
| ' destinations:', | |
| ' - target: amplitude-prod', | |
| ' - target: dwh', | |
| '', | |
| ' # All sn_ events to Amplitude prod', | |
| ' all_sn_to_amplitude_prod:', | |
| ' priority: 60', | |
| ' conditions:', | |
| ' - type: regex', | |
| ' by: event_name', | |
| ' pattern: "^sn_(.+)"', | |
| ' destinations:', | |
| ' - target: amplitude-prod', | |
| '', | |
| ' # Editor events from Android also to Android Amplitude project', | |
| ' editor_android:', | |
| ' priority: 80', | |
| ' conditions:', | |
| ' - type: regex', | |
| ' by: event_name', | |
| ' pattern: "sn_editor_(.+)"', | |
| ' - type: match', | |
| ' by: field', | |
| ' field: source.platform', | |
| ' value: Android', | |
| ' destinations:', | |
| ' - target: amplitude-android', | |
| '', | |
| ' # All iOS events to iOS Amplitude project', | |
| ' ios_all_events:', | |
| ' priority: 70', | |
| ' conditions:', | |
| ' - type: match', | |
| ' by: field', | |
| ' field: source.platform', | |
| ' value: iOS', | |
| ' destinations:', | |
| ' - target: amplitude-ios', | |
| '', | |
| ' # Low-priority catch-all to Amplitude (tests priority filtering)', | |
| ' low_priority_catch_all:', | |
| ' priority: 15', | |
| ' conditions:', | |
| ' - type: regex', | |
| ' by: event_name', | |
| ' pattern: ".*"', | |
| ' destinations:', | |
| ' - target: amplitude-prod', | |
| '', | |
| ' # Default: everything to DWH', | |
| ' default_dwh:', | |
| ' priority: 10', | |
| ' conditions:', | |
| ' - type: regex', | |
| ' by: event_name', | |
| ' pattern: ".*"', | |
| ' destinations:', | |
| ' - target: dwh', | |
| ].join('\n'); | |
| var PRESETS = { | |
| editor_web_rich: { | |
| event: { | |
| "event_name": "sn_editor_open", | |
| "identity": { "user_id": "user_abc123", "device_id": "dev_web_001", "session_id": 1712570400000 }, | |
| "source": { "platform": "Web", "os_name": "Chrome", "os_version": "124.0", "device_model": "Macintosh", "device_brand": "Apple", "language": "en", "library": "amplitude-ts/2.11.0", "ip": "1.2.3.4", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "insert_id": "ins_001", "app_version": "3.45.0" }, | |
| "event_properties": { "document_id": "doc_555", "rows_amount": "42", "file_type": "pdf", "has_template": "true", "page_count": "12", "has_fields": "true" }, | |
| "user_properties": { "subscription": "premium", "is_premium": true, "plan_name": "business", "country": "US", "city": "San Francisco", "dma": "807", "region": "California", "device_type": "Desktop", "browser": "Chrome", "browser_version": "124.0" } | |
| } | |
| }, | |
| editor_android: { | |
| event: { | |
| "event_name": "sn_editor_save", | |
| "identity": { "user_id": "user_mob_1", "device_id": "android_xyz_999", "session_id": 1712571000000 }, | |
| "source": { "platform": "Android", "os_name": "Android", "os_version": "14", "language": "uk", "library": "amplitude-android/1.5.0", "ip": "5.6.7.8", "insert_id": "ins_002", "device_brand": "Samsung", "device_manufacturer": "Samsung", "device_model": "Galaxy S24", "carrier": "Kyivstar", "app_version": "2.12.1" }, | |
| "event_properties": { "document_id": "doc_777", "page_count": "3", "save_type": "auto" }, | |
| "user_properties": { "subscription": "free", "is_premium": false, "country": "UA", "city": "Kyiv", "language": "uk" } | |
| } | |
| }, | |
| ios_document_signed: { | |
| event: { | |
| "event_name": "sn_document_signed", | |
| "identity": { "user_id": "user_ios_5", "device_id": "iphone_abc_555", "session_id": 1712572000000 }, | |
| "source": { "platform": "iOS", "os_name": "iOS", "os_version": "17.4", "language": "en", "library": "amplitude-swift/1.8.0", "ip": "10.0.0.1", "insert_id": "ins_003", "device_model": "iPhone 15 Pro", "device_brand": "Apple", "idfv": "idfv_abc123", "app_version": "2.11.0" }, | |
| "event_properties": { "document_id": "doc_999", "signer_role": "primary", "signature_type": "drawn", "is_last_signer": "true" }, | |
| "user_properties": { "subscription": "business", "is_premium": true, "plan_name": "enterprise", "country": "GB", "city": "London", "region": "England" } | |
| } | |
| }, | |
| legacy_upload: { | |
| event: { | |
| "event_name": "upload_document", | |
| "identity": { "user_id": "user_legacy_42" }, | |
| "source": { "platform": "Web", "input_type": "legacy" }, | |
| "event_properties": { "document_id": "doc_100", "file_type": "docx" }, | |
| "user_properties": {} | |
| } | |
| }, | |
| legacy_send_to_sign: { | |
| event: { | |
| "event_name": "sn_send_to_sign", | |
| "identity": { "user_id": "user_pm_007" }, | |
| "source": { "platform": "Web", "input_type": "legacy" }, | |
| "event_properties": { | |
| "document_id": "doc_200", | |
| "rows_amount": "5", | |
| "signers_count": "3", | |
| "has_payment": "false", | |
| "has_attachments": "true", | |
| "signing_order": "sequential", | |
| "template_id": "tmpl_50", | |
| "is_bulk_send": "false", | |
| "expiration_days": "30", | |
| "reminder_enabled": "true", | |
| "has_cc_recipients": "true", | |
| "cc_count": "2" | |
| }, | |
| "user_properties": { "subscription": "premium", "is_premium": true, "plan_name": "business" } | |
| } | |
| }, | |
| amplitude_with_dbparams: { | |
| event: { | |
| "event_name": "sn_editor_field_added", | |
| "identity": { "user_id": "user_web_88", "device_id": "dev_web_088", "session_id": 1712574000000 }, | |
| "source": { "platform": "Web", "os_name": "Firefox", "os_version": "125.0", "language": "de", "library": "amplitude-ts/2.11.0", "ip": "85.10.20.30", "insert_id": "ins_007", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0)" }, | |
| "event_properties": { | |
| "document_id": "doc_350", | |
| "field_type": "signature", | |
| "page_number": "2", | |
| "has_convert_to_template_button": "true", | |
| "ui_session_id": "sess_xyz_999" | |
| }, | |
| "user_properties": { "subscription": "starter", "is_premium": false, "plan_name": "starter", "country": "DE", "city": "Berlin", "device_type": "Desktop", "browser": "Firefox", "browser_version": "125.0", "dma": "0" } | |
| } | |
| }, | |
| backend_qless: { | |
| event: { | |
| "event_name": "sn_api_dev_mode_limit_created", | |
| "identity": { "user_id": "user_api_integrator_5" }, | |
| "source": { "platform": "backend", "library": "guzzle/7.0", "application_id": "app_zapier_prod" }, | |
| "event_properties": { "endpoint": "/v2/documents", "method": "POST", "limit_type": "rate", "current_count": "101", "max_allowed": "100" }, | |
| "user_properties": { "subscription": "api_basic", "is_premium": false, "plan_name": "api_basic" } | |
| } | |
| }, | |
| guest_no_userid: { | |
| event: { | |
| "event_name": "sn_editor_view", | |
| "identity": { "device_id": "anon_dev_xyz_777", "session_id": 1712575000000 }, | |
| "source": { "platform": "Web", "os_name": "Safari", "os_version": "17.4", "language": "fr", "library": "amplitude-ts/2.11.0", "ip": "88.99.10.20", "insert_id": "ins_anon_001", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15" }, | |
| "event_properties": { "document_id": "doc_public_link_800", "access_type": "signing_link", "is_guest": "true" }, | |
| "user_properties": {} | |
| } | |
| } | |
| }; | |
| function loadPreset(key) { | |
| if (!key || !PRESETS[key]) return; | |
| document.getElementById('input').value = JSON.stringify(PRESETS[key].event, null, 2); | |
| document.getElementById('rules').value = SHARED_CONFIG_YAML; | |
| doEvaluate(); | |
| } | |
| function clearAll() { | |
| document.getElementById('input').value = ''; | |
| document.getElementById('rules').value = ''; | |
| clearElement(document.getElementById('results')); | |
| document.getElementById('stats-bar').style.display = 'none'; | |
| document.getElementById('input-status').textContent = 'waiting'; | |
| document.getElementById('input-status').style.color = ''; | |
| document.getElementById('rules-status').textContent = 'waiting'; | |
| document.getElementById('rules-status').style.color = ''; | |
| document.getElementById('result-count').textContent = '-'; | |
| } | |
| // =========== CODEMIRROR SETUP =========== | |
| var cmInput = CodeMirror.fromTextArea(document.getElementById('input'), { | |
| mode: { name: 'javascript', json: true }, | |
| lineNumbers: true, tabSize: 2, matchBrackets: true, lineWrapping: true | |
| }); | |
| var cmRules = CodeMirror.fromTextArea(document.getElementById('rules'), { | |
| mode: 'yaml', | |
| lineNumbers: true, tabSize: 2, lineWrapping: true | |
| }); | |
| // Override getters to use CodeMirror | |
| var origEval = doEvaluate; | |
| doEvaluate = function() { | |
| document.getElementById('input').value = cmInput.getValue(); | |
| document.getElementById('rules').value = cmRules.getValue(); | |
| origEval(); | |
| }; | |
| var origLoadPreset = loadPreset; | |
| loadPreset = function(key) { | |
| if (!key || !PRESETS[key]) return; | |
| cmInput.setValue(JSON.stringify(PRESETS[key].event, null, 2)); | |
| cmRules.setValue(SHARED_CONFIG_YAML); | |
| doEvaluate(); | |
| }; | |
| var origClear = clearAll; | |
| clearAll = function() { | |
| cmInput.setValue(''); | |
| cmRules.setValue(''); | |
| clearElement(document.getElementById('results')); | |
| document.getElementById('stats-bar').style.display = 'none'; | |
| document.getElementById('input-status').textContent = 'waiting'; | |
| document.getElementById('input-status').style.color = ''; | |
| document.getElementById('rules-status').textContent = 'waiting'; | |
| document.getElementById('rules-status').style.color = ''; | |
| document.getElementById('result-count').textContent = '-'; | |
| }; | |
| document.getElementById('btn-eval').addEventListener('click', doEvaluate); | |
| document.getElementById('preset-select').addEventListener('change', function() { loadPreset(this.value); }); | |
| document.getElementById('btn-clear').addEventListener('click', clearAll); | |
| document.addEventListener('keydown', function(e) { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); doEvaluate(); } }); | |
| // Load first preset on page open | |
| document.getElementById('preset-select').value = 'editor_web_rich'; | |
| loadPreset('editor_web_rich'); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment