T7.10 - D5: one analytics implementation, and its report on the admin console
Usage analytics existed as five of the nine colliding globals creator-frame.md counted (ANALYTICS_KEY, analyticsLoad, analyticsSave, downloadAnalytics, showAnalytics), twice - and the wizard's copy had no caller, because the button lived on the creator. The admin console had a THIRD private reader (usageLoad/downloadUsage) that only saw the wizard's key. Now: ONE core, html/wp-usage.js (window.WPUsage: load/save/track/download + the two pre-move storage keys, verbatim). The creator and wizard keep only a thin track() wrapper - page state like the creator's dev-mode pause belongs to the page - and record exactly what they recorded before, under the same keys, so everything captured before this task still reads (probe plants a legacy-format event and finds it in the report). The "Usage data" button left the creator toolbar; the report lives in admin.html's usage card, covering BOTH tools with a download each, behind the same admin gate as the rest of the console (a non-admin sees the denied card and nothing else), usable at 390px. Two probes re-pointed, both with the reason in the code: - cards_check pinned admin.js byte-identical to HEAD - right for T6.5, but as a standing probe it would fail every legitimate later edit; D5 targets admin.js by name. A7's localization is protected by the feature checks and the end-to-end drive, plus a wiring assertion on the block itself. - frame_check listed "Usage data" among the toolbar buttons that must be visible; it now asserts the button is GONE, so the duplicate cannot quietly return. Verification (each probe run alone): NEW tests/usage_check.py 15/15 (grep half: WPUsage defined once, no page touches the keys directly, none of the five globals survives anywhere). Regressions: cards_check ALL PASS, frame_check 39/39. Items: D5 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2337,69 +2337,19 @@ function loadStepComments(){
|
||||
// Lightweight usage analytics stored in localStorage so the tool owner can review
|
||||
// engagement over time. No field VALUES are stored (field-edit events record only
|
||||
// the field id), keeping captured data non-sensitive.
|
||||
const ANALYTICS_KEY = 'wp_suite_analytics_v1';
|
||||
// D5 / T7.10: the analytics implementation lives in wp-usage.js and the report
|
||||
// on the admin console. The wizard's own copy of showAnalytics() never had a
|
||||
// caller here - the button lived on the creator - and once B7 dissolved the
|
||||
// frame the duplicate sat in the same document as five colliding globals.
|
||||
// This page only records; dwell tracking keeps its page-local state below.
|
||||
let _stepEnter = Date.now();
|
||||
const _session = 's_' + Date.now().toString(36) + Math.random().toString(36).slice(2,6);
|
||||
|
||||
function analyticsLoad(){
|
||||
try { return JSON.parse(localStorage.getItem(ANALYTICS_KEY)) || {events:[]}; }
|
||||
catch(e){ return {events:[]}; }
|
||||
}
|
||||
function analyticsSave(data){
|
||||
try { localStorage.setItem(ANALYTICS_KEY, JSON.stringify(data)); }
|
||||
catch(e){ /* storage unavailable — degrade silently */ }
|
||||
}
|
||||
function track(event, detail){
|
||||
try {
|
||||
const data = analyticsLoad();
|
||||
data.events.push({ ts: new Date().toISOString(), session: _session, event, detail: detail||null });
|
||||
if(data.events.length > 5000) data.events = data.events.slice(-5000);
|
||||
analyticsSave(data);
|
||||
} catch(e){}
|
||||
WPUsage.track(WPUsage.KEYS.wizard, event, detail);
|
||||
}
|
||||
function trackStepDwell(){
|
||||
const ms = Date.now() - _stepEnter;
|
||||
if(ms > 400 && ms < 1000*60*60) track('step_dwell', {step: currentStep, ms});
|
||||
_stepEnter = Date.now();
|
||||
}
|
||||
function analyticsSummary(){
|
||||
const data = analyticsLoad();
|
||||
const byEvent = {}, byStep = {}, dwell = {}, sessions = new Set();
|
||||
data.events.forEach(e=>{
|
||||
byEvent[e.event] = (byEvent[e.event]||0)+1;
|
||||
sessions.add(e.session);
|
||||
if(e.event==='step_view' && e.detail) byStep[e.detail.step]=(byStep[e.detail.step]||0)+1;
|
||||
if(e.event==='step_dwell' && e.detail){ dwell[e.detail.step]=(dwell[e.detail.step]||0)+e.detail.ms; }
|
||||
});
|
||||
return {total:data.events.length, sessions:sessions.size, byEvent, byStep, dwell, first:data.events[0]?.ts, last:data.events[data.events.length-1]?.ts};
|
||||
}
|
||||
function showAnalytics(){
|
||||
const s = analyticsSummary();
|
||||
const fmtMin = ms => (ms/60000).toFixed(1)+' min';
|
||||
let txt = `USAGE LOGS\n\nSessions: ${s.sessions} Events: ${s.total}\nRange: ${s.first?new Date(s.first).toLocaleString():'—'} → ${s.last?new Date(s.last).toLocaleString():'—'}\n\nStep views:\n`;
|
||||
for(let i=1;i<=LAST_STEP;i++) txt += ` Step ${i}: ${s.byStep[i]||0} views` + (s.dwell[i]?`, ${fmtMin(s.dwell[i])} total`:'') + `\n`;
|
||||
txt += `\nActions:\n`;
|
||||
Object.keys(s.byEvent).filter(k=>!['step_view','step_dwell','field_edit'].includes(k)).forEach(k=> txt += ` ${k}: ${s.byEvent[k]}\n`);
|
||||
txt += ` field edits: ${s.byEvent['field_edit']||0}\n`;
|
||||
// Was a native confirmation carrying the whole summary as its body — a wall of
|
||||
// text in a dialog with one OK button. The summary is the useful part, so it is
|
||||
// shown, and the download is offered as an action beside it rather than as the
|
||||
// only way to dismiss the message.
|
||||
//
|
||||
// Note this function has no caller in the wizard's markup: the "Usage data"
|
||||
// button lives on the creator, which has its own showAnalytics(). Converted
|
||||
// rather than deleted because deleting a feature is not what T5.8 was asked to
|
||||
// do, and its dialog counts toward the number this task has to drive to zero.
|
||||
wizardToast(txt.replace(/\n+/g, ' · ').trim(),
|
||||
{action: {label: 'Download the full event log', fn: downloadAnalytics}});
|
||||
}
|
||||
function downloadAnalytics(){
|
||||
const data = analyticsLoad();
|
||||
const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'});
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'wp-suite-usage-' + new Date().toISOString().slice(0,10) + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
track('analytics_exported');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user