diff --git a/docs/local-analytics.md b/docs/local-analytics.md index c7891b7..5d659a7 100644 --- a/docs/local-analytics.md +++ b/docs/local-analytics.md @@ -24,7 +24,11 @@ local Umami service at `http://127.0.0.1:3100`; it does not contact 103/105. ``` 4. Restart the local Memind server. Full generated HTML pages will receive a - same-origin `/analytics/script.js` tracker and a `page_view` event with + same-origin `/analytics/script.js` tracker. The tracker identifies the + visitor with a stable pseudonymous owner ID before sending a standard Umami + page view, so Users and Pageviews are populated. The Identify properties + include the readable Memind username and current public page URL for + operational analytics. Click, form, scroll, and engagement events retain the pseudonymous `owner_id`, `page_id`, and `channel` dimensions. The integration is fail-open: missing configuration, disabled analytics, or a diff --git a/mindspace-analytics.mjs b/mindspace-analytics.mjs index f3839e4..7312341 100644 --- a/mindspace-analytics.mjs +++ b/mindspace-analytics.mjs @@ -95,10 +95,10 @@ export function injectMindSpaceAnalytics(html, { if (source.includes(ANALYTICS_MARKER)) return source; const owner = pseudonymizeAnalyticsId(ownerId, config.idSecret); if (!owner) return source; - // Public page source must not contain a readable account name. The stable - // pseudonym and coarse plan segment are sufficient for page analytics; - // readable labels are reserved for server-originated events only. - const metadata = { owner_id: owner, owner_segment: String(ownerSegment || 'unknown'), page_id: String(pageId || ''), publication_id: String(publicationId || ''), channel }; + // The stable pseudonym remains the Umami identity key. The readable username + // is an explicitly enabled analytics property so operators can recognize the + // Memind user, while the current public page URL is resolved in the browser. + const metadata = { owner_id: owner, username: resolveAnalyticsOwnerLabel({ displayName: ownerLabel }), owner_segment: String(ownerSegment || 'unknown'), page_id: String(pageId || ''), publication_id: String(publicationId || ''), channel }; const attrs = [ ANALYTICS_MARKER, `data-website-id="${config.websiteId.replaceAll('"', '"')}"`, @@ -106,7 +106,7 @@ export function injectMindSpaceAnalytics(html, { `data-host-url="${config.hostPath}"`, ]; if (config.domains) attrs.push(`data-domains="${config.domains.replaceAll('"', '"')}"`); - const block = ``; + const block = ``; if (/<\/head>/i.test(source)) return source.replace(/<\/head>/i, `${block}`); return source.replace(/ { assert.match(out, /src="\/analytics\/script\.js"/); assert.match(out, /data-host-url="\/analytics"/); assert.match(out, /data-auto-track="false"/); + assert.match(out, /window\.umami\.identify\(d\.owner_id,\{username:d\.username,memind_page_url:location\.href,owner_segment:d\.owner_segment,channel:d\.channel\}\)/); + assert.match(out, /function pageview\(\).*window\.umami\.track\(\)/); + assert.ok(out.indexOf('identify();pageview();') > 0); + assert.doesNotMatch(out, /t\('page_view'\)/); assert.match(out, /page_id/); assert.match(out, /owner_segment/); assert.doesNotMatch(out, /owner_label/); - assert.doesNotMatch(out, /张三/); + assert.match(out, /"username":"张三"/); assert.match(out, /page_click/); assert.match(out, /page_form_submit/); assert.match(out, /page_scroll_/); @@ -76,6 +81,55 @@ test('injects one local same-origin tracker with page dimensions', () => { assert.equal(injectMindSpaceAnalytics(out, { ownerId: 'user-123', config: { enabled: true, websiteId: 'local-website', idSecret: 'secret' } }), out); }); +test('identifies the pseudonymous owner before sending a standard page view', () => { + const out = injectMindSpaceAnalytics('', { + ownerId: 'user-123', + ownerSegment: 'plan:pro', + ownerLabel: '张三', + channel: 'h5', + config: { + enabled: true, + websiteId: 'local-website', + idSecret: 'secret', + scriptPath: '/analytics/script.js', + hostPath: '/analytics', + }, + }); + const inlineScript = out.match(/