Step 1 — Log in to MinistryPlatform

Use the login widget below to authenticate against mpsandbox.archomaha.org. Once logged in, the auth token is written to this page’s localStorage (same-origin), and the engagement tracker below can call MP stored procedures on your behalf.

#mp-engagement-test-container { font-family: system-ui, -apple-system, sans-serif; max-width: 880px; margin: 1em auto; color: #1f2937; } #mp-engagement-test-container h1 { color: #1f3a5f; border-bottom: 2px solid #1f3a5f; padding-bottom: 0.3em; } #mp-engagement-test-container h2 { color: #1f3a5f; margin-top: 2em; } #mp-engagement-test-container .status-box { padding: 1em 1.2em; margin: 1em 0; border-radius: 6px; } #mp-engagement-test-container .status-box.info { background: #e7f3ff; border: 1px solid #91caff; } #mp-engagement-test-container .status-box.pending { background: #fffbe6; border: 1px solid #ffe58f; } #mp-engagement-test-container .status-box.success { background: #f6ffed; border: 1px solid #b7eb8f; } #mp-engagement-test-container .status-box.failure { background: #fff1f0; border: 1px solid #ffa39e; } #mp-engagement-test-container a[data-mp-track], #mp-engagement-test-container button[data-mp-track] { display: inline-block; padding: 0.5em 0.9em; margin: 0.3em; background: #1f3a5f; color: white; text-decoration: none; border-radius: 4px; border: none; font-size: 0.95em; cursor: pointer; } #mp-engagement-test-container a[data-mp-track]:hover, #mp-engagement-test-container button[data-mp-track]:hover { background: #2c5689; } #mp-engagement-test-container .pill-stewardship { background: #2dc26b !important; } #mp-engagement-test-container .pill-sacrament { background: #9d2dc2 !important; } #mp-engagement-test-container .pill-bulletin { background: #c2402d !important; } #mp-engagement-test-container .pill-directory { background: #c28e2d !important; } #mp-engagement-test-container .pill-meeting { background: #5b21b6 !important; } #mp-engagement-test-container #event-log { background: #f5f5f5; padding: 1em; border-radius: 4px; max-height: 400px; overflow-y: auto; font-family: ui-monospace, monospace; font-size: 0.85em; } #mp-engagement-test-container .event-row { padding: 0.4em 0; border-bottom: 1px solid #e5e7eb; } #mp-engagement-test-container .event-row:last-child { border-bottom: none; } #mp-engagement-test-container .event-row.success { color: #166534; } #mp-engagement-test-container .event-row.error { color: #991b1b; } #mp-engagement-test-container details { margin-top: 1em; } #mp-engagement-test-container code { background: #f5f5f5; padding: 1px 4px; border-radius: 3px; } #mp-engagement-test-container .session-display { display: inline-block; padding: 0.2em 0.6em; background: #ede9fe; border: 1px solid #c4b5fd; border-radius: 4px; font-family: ui-monospace, monospace; font-size: 0.82em; color: #5b21b6; } #mp-engagement-test-container kbd { background: #f5f5f5; border: 1px solid #d1d5db; border-radius: 3px; padding: 1px 5px; font-size: 0.85em; font-family: ui-monospace, monospace; }

Step 2 — MP Web Engagement Tracker (v2: MP-native FKs)

What this section tests: the full click → engagement-tracker JS → widget API → api_Custom_LogClick SP → Web_Click_Logs INSERT pipeline using the v2 schema with User_ID, Contact_ID, Ministry_ID, Group_ID, Event_ID, and Session_ID. Click any tracked element below; the Event Log shows the SP response. Then run 05-verify-rows.sql in SSMS to confirm rows physically landed.
Auth check: waiting for MP login…

Browser session: (generating…) — same UUID for every click in this tab. Close+reopen the tab to get a new one.

Tracked Test Links

Each link uses the data-mp-* attribute family. The JS reads them at click time and passes the corresponding FK values to the SP. FK values are placeholders — to test real FK attribution, edit the data-mp-ministry-id / data-mp-group-id / data-mp-event-id attributes below to point at actual records in mp-sandbox. Empty/missing attributes pass NULL, which the SP accepts gracefully.

Stewardship Resources Giving History Marriage Prep Baptism Prep This Week’s Bulletin Member Directory (no Ministry) SBE Finance Meeting – 26-05-09 (richest example)

Event Log

Initialized — waiting for clicks…
Configuration & data attribute reference

Page-level default: window.MP_PAGE_MINISTRY_ID set in the JS below — applies as Ministry_ID for any tracked element that doesn’t have its own override.

Element-level attributes (override page-level for that element):

  • data-mp-track — required; marks element as trackable
  • data-mp-ministry-id — overrides page-level Ministry default → @Ministry_ID
  • data-mp-group-id — optional → @Group_ID
  • data-mp-event-id — optional → @Event_ID
  • data-mp-label — optional, overrides text-content extraction → @Element_Label

If clicks register here but no rows land in Web_Click_Logs, check:

  1. Browser DevTools → Network tab — look for /widgets/api/CustomWidget requests; check status codes
  2. If 404 on the vanity host: switch API_HOST in the script below to the cloud-subdomain prefix
  3. If 401/403: 03-register-procedure.sql didn’t fully run; verify both dp_API_Procedures and dp_Role_API_Procedures rows exist
  4. If 500 with FK violation: a data-mp-*-id attribute references a non-existent record. Either NULL it out or fix the value.
  5. If CORS error: pjcrusadersdev origin may not be on MP’s allowlist — investigate the actual error in the console

window.MPCustomWidgetsConfig = { mpHost: ‘mpsandbox.archomaha.org’ }; // Page-level Ministry default (was a meta tag in the static HTML; // here it’s a JS variable since meta tags belong in not ). // Set to a numeric Ministry_ID to declare “this whole page belongs to that Ministry”; // null means no default (element-level data-mp-ministry-id required for attribution). window.MP_PAGE_MINISTRY_ID = null; const API_HOST = ‘mpsandbox.archomaha.org’; const SP_NAME = ‘api_Custom_LogClick’; const SITE_DOMAIN = window.location.hostname; function getOrCreateSessionId() { const STORAGE_KEY = ‘mp-engagement-session-id’; let sid = sessionStorage.getItem(STORAGE_KEY); if (!sid) { sid = (crypto && crypto.randomUUID) ? crypto.randomUUID() : (‘xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx’.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === ‘x’ ? r : (r & 0x3 | 0x8)).toString(16); })); sessionStorage.setItem(STORAGE_KEY, sid); } return sid; } const SESSION_ID = getOrCreateSessionId(); const PAGE_MINISTRY_ID = (typeof window.MP_PAGE_MINISTRY_ID === ‘number’) ? window.MP_PAGE_MINISTRY_ID : null; function logEvent(message, level) { const log = document.getElementById(‘event-log’); if (!log) return; const row = document.createElement(‘div’); row.className = ‘event-row ‘ + (level === ‘error’ ? ‘error’ : level === ‘success’ ? ‘success’ : ”); const ts = new Date().toLocaleTimeString(); row.textContent = ‘[‘ + ts + ‘] ‘ + message; log.insertBefore(row, log.firstChild); } function parseIntOrNull(v) { if (v === null || v === undefined) return null; const s = String(v).trim(); if (!s) return null; const n = parseInt(s, 10); return Number.isFinite(n) ? n : null; } function elementType(el) { const t = (el.tagName || ”).toLowerCase(); if (t === ‘a’) return ‘link’; if (t === ‘button’) return ‘button’; return t || ‘unknown’; } function buildSpParams(el) { const ministryId = parseIntOrNull(el.getAttribute(‘data-mp-ministry-id’)); const effectiveMinistryId = (ministryId !== null) ? ministryId : PAGE_MINISTRY_ID; const groupId = parseIntOrNull(el.getAttribute(‘data-mp-group-id’)); const eventId = parseIntOrNull(el.getAttribute(‘data-mp-event-id’)); const labelOverride = el.getAttribute(‘data-mp-label’); const elementLabel = (labelOverride || (el.textContent || ”).trim()).substring(0, 200); const parts = [ ‘@Site_Domain=’ + SITE_DOMAIN, ‘@Page_URL=’ + window.location.href, ‘@Page_Title=’ + (document.title || ”).substring(0, 200), ‘@Element_Type=’ + elementType(el), ‘@Element_Label=’ + elementLabel, ‘@Element_Target=’ + (el.href || el.getAttribute(‘formaction’) || ”), effectiveMinistryId !== null ? ‘@Ministry_ID=’ + effectiveMinistryId : null, groupId !== null ? ‘@Group_ID=’ + groupId : null, eventId !== null ? ‘@Event_ID=’ + eventId : null, ‘@Session_ID=’ + SESSION_ID, ‘@User_Agent=’ + (navigator.userAgent || ”).substring(0, 250), ‘@Referrer_URL=’ + (document.referrer || ”).substring(0, 500) ].filter(Boolean); return parts.join(‘&’); } function buildApiUrl(spParams, authToken) { return ‘https://’ + API_HOST + ‘/widgets/api/CustomWidget’ + ‘?storedProcedure=’ + encodeURIComponent(SP_NAME) + ‘&spParams=’ + encodeURIComponent(spParams) + ‘&userData=’ + encodeURIComponent(authToken) + ‘&requireUser=true’ + ‘&cacheData=false’; } async function logClick(el) { const authToken = localStorage.getItem(‘mpp-widgets_AuthToken’); if (!authToken) { logEvent(‘Skipped click on “‘ + (el.textContent || ”).trim() + ‘” — no auth token (log in via the widget above)’, ‘error’); return; } const spParams = buildSpParams(el); const url = buildApiUrl(spParams, authToken); const labelDisplay = (el.getAttribute(‘data-mp-label’) || (el.textContent || ”).trim()).substring(0, 50); try { const response = await fetch(url, { method: ‘GET’, credentials: ‘include’ }); if (response.ok) { const data = await response.json().catch(function () { return null; }); const row = data && (data.DataSet1 && data.DataSet1[0] || data[0] && data[0][0] || null); if (row) { const wcli = row.Web_Click_Log_ID == null ? ‘?’ : row.Web_Click_Log_ID; const cid = row.Contact_ID == null ? ‘null’ : row.Contact_ID; const mid = row.Ministry_ID == null ? ‘null’ : row.Ministry_ID; const gid = row.Group_ID == null ? ‘null’ : row.Group_ID; const eid = row.Event_ID == null ? ‘null’ : row.Event_ID; logEvent(‘✓ “‘ + labelDisplay + ‘” → WCLI=’ + wcli + ‘, Contact=’ + cid + ‘, Ministry=’ + mid + ‘, Group=’ + gid + ‘, Event=’ + eid + ‘ (HTTP ‘ + response.status + ‘)’, ‘success’); } else { logEvent(‘✓ “‘ + labelDisplay + ‘” → HTTP ‘ + response.status + ‘ (no row in response)’, ‘success’); } } else { const txt = await response.text().catch(function () { return ”; }); logEvent(‘✗ “‘ + labelDisplay + ‘” → HTTP ‘ + response.status + ‘ ‘ + response.statusText + ‘: ‘ + txt.substring(0, 200), ‘error’); } } catch (err) { logEvent(‘✗ “‘ + labelDisplay + ‘” → fetch error: ‘ + err.message, ‘error’); } } function refreshAuthStatus() { const authToken = localStorage.getItem(‘mpp-widgets_AuthToken’); const expires = localStorage.getItem(‘mpp-widgets_ExpiresAfter’); const box = document.getElementById(‘auth-status’); if (!box) return; if (authToken) { box.className = ‘status-box success’; box.innerHTML = ‘Auth check: token present (expires ‘ + (expires || ‘unknown’) + ‘). Click the test links below.’; } else { box.className = ‘status-box pending’; box.innerHTML = ‘Auth check: no token in localStorage. Log in via the MP Login widget above.’; } } document.addEventListener(‘DOMContentLoaded’, function () { const sd = document.getElementById(‘session-display’); if (sd) sd.textContent = SESSION_ID; refreshAuthStatus(); setInterval(refreshAuthStatus, 5000); const tracked = document.querySelectorAll(‘[data-mp-track]’); tracked.forEach(function (el) { el.addEventListener(‘click’, function (e) { e.preventDefault(); logClick(el); }); }); const pageMinistryNote = (PAGE_MINISTRY_ID === null) ? ‘no page-level Ministry default set’ : ‘page-level Ministry_ID = ‘ + PAGE_MINISTRY_ID; logEvent(‘Engagement Tracker ready — ‘ + tracked.length + ‘ tracked elements, session ‘ + SESSION_ID.substring(0, 8) + ‘…, ‘ + pageMinistryNote, ‘info’); });