255 lines
9.3 KiB
JavaScript
255 lines
9.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import vm from 'node:vm';
|
|
|
|
import {
|
|
buildProductAnalyticsContext,
|
|
injectMindSpaceAnalytics,
|
|
pseudonymizeAnalyticsId,
|
|
resolveAnalyticsIdentity,
|
|
resolveAnalyticsPlan,
|
|
resolveAnalyticsOwnerSegment,
|
|
resolveAnalyticsOwnerLabel,
|
|
resolveMindSpaceAnalyticsConfig,
|
|
resolveProductAnalyticsConfig,
|
|
sendMindSpaceAnalyticsEvent,
|
|
} from './mindspace-analytics.mjs';
|
|
|
|
test('product analytics uses a separate website and the existing pseudonymization secret', () => {
|
|
const base = resolveMindSpaceAnalyticsConfig({
|
|
MEMIND_ANALYTICS_ENABLED: 'true',
|
|
MEMIND_ANALYTICS_WEBSITE_ID: 'generated-pages',
|
|
MEMIND_ANALYTICS_ID_SECRET: 'local-secret',
|
|
});
|
|
const config = resolveProductAnalyticsConfig({
|
|
MEMIND_PRODUCT_ANALYTICS_ENABLED: 'true',
|
|
MEMIND_PRODUCT_ANALYTICS_WEBSITE_ID: 'product-shell',
|
|
}, base);
|
|
assert.equal(config.enabled, true);
|
|
assert.equal(config.websiteId, 'product-shell');
|
|
assert.notEqual(config.websiteId, base.websiteId);
|
|
const context = buildProductAnalyticsContext({
|
|
config,
|
|
user: { id: 'user-123', displayName: '张三', role: 'user', planType: 'pro' },
|
|
});
|
|
assert.deepEqual(context.identity, {
|
|
distinctId: pseudonymizeAnalyticsId('user-123', 'local-secret'),
|
|
username: '张三',
|
|
ownerSegment: 'plan:pro',
|
|
planType: 'pro',
|
|
channel: 'h5',
|
|
surface: 'product',
|
|
identityMode: 'pseudonymous',
|
|
});
|
|
});
|
|
|
|
test('product analytics context stays public-safe before login', () => {
|
|
const context = buildProductAnalyticsContext({
|
|
config: {
|
|
enabled: true,
|
|
websiteId: 'product-shell',
|
|
idSecret: 'local-secret',
|
|
scriptPath: '/analytics/script.js',
|
|
hostPath: '/analytics',
|
|
},
|
|
});
|
|
assert.equal(context.enabled, true);
|
|
assert.equal(context.websiteId, 'product-shell');
|
|
assert.equal(context.identity, null);
|
|
assert.equal('idSecret' in context, false);
|
|
});
|
|
|
|
test('analytics config is disabled unless explicitly enabled and configured', () => {
|
|
assert.equal(resolveMindSpaceAnalyticsConfig({ MEMIND_ANALYTICS_ENABLED: 'true' }).enabled, false);
|
|
assert.equal(resolveMindSpaceAnalyticsConfig({
|
|
MEMIND_ANALYTICS_ENABLED: 'true',
|
|
MEMIND_ANALYTICS_WEBSITE_ID: 'local-website',
|
|
}).enabled, false);
|
|
const config = resolveMindSpaceAnalyticsConfig({
|
|
MEMIND_ANALYTICS_ENABLED: 'true',
|
|
MEMIND_ANALYTICS_URL: 'http://127.0.0.1:3200',
|
|
MEMIND_ANALYTICS_WEBSITE_ID: 'local-website',
|
|
MEMIND_ANALYTICS_ID_SECRET: 'local-secret',
|
|
});
|
|
assert.equal(config.enabled, true);
|
|
assert.equal(config.analyticsUrl, 'http://127.0.0.1:3200');
|
|
assert.equal(config.hostPath, '/analytics');
|
|
});
|
|
|
|
test('owner ids are stable pseudonyms and never expose the source id', () => {
|
|
const first = pseudonymizeAnalyticsId('user-123', 'secret');
|
|
assert.equal(first, pseudonymizeAnalyticsId('user-123', 'secret'));
|
|
assert.notEqual(first, 'user-123');
|
|
assert.notEqual(first, pseudonymizeAnalyticsId('user-456', 'secret'));
|
|
});
|
|
|
|
test('approved raw identity mode preserves the stable Memind user id', () => {
|
|
const config = { identityMode: 'raw', idSecret: 'unused' };
|
|
assert.equal(resolveAnalyticsIdentity('user-123', config), 'user-123');
|
|
assert.equal(resolveAnalyticsIdentity(' user-123\n', config), 'user-123');
|
|
});
|
|
|
|
test('owner segments come from server-side Memind user profile data', () => {
|
|
assert.equal(resolveAnalyticsOwnerSegment({ role: 'admin' }), 'admin');
|
|
assert.equal(resolveAnalyticsOwnerSegment({ role: 'user', planType: 'pro' }), 'plan:pro');
|
|
assert.equal(resolveAnalyticsOwnerSegment({ role: 'user' }), 'plan:free');
|
|
assert.equal(resolveAnalyticsPlan({ role: 'user', planType: 'pro' }), 'pro');
|
|
assert.equal(resolveAnalyticsPlan({ role: 'user' }), 'free');
|
|
});
|
|
|
|
test('owner labels are readable but bounded and stripped of control characters', () => {
|
|
assert.equal(resolveAnalyticsOwnerLabel({ displayName: '张三\n管理员' }), '张三 管理员');
|
|
assert.equal(resolveAnalyticsOwnerLabel({}), '未命名用户');
|
|
});
|
|
|
|
test('injects one local same-origin tracker with page dimensions', () => {
|
|
const html = '<!doctype html><html><head><title>Demo</title></head><body><h1>Demo</h1></body></html>';
|
|
const out = injectMindSpaceAnalytics(html, {
|
|
ownerId: 'user-123',
|
|
ownerLabel: '张三',
|
|
pageId: 'page-1',
|
|
publicationId: 'pub-1',
|
|
planType: 'pro',
|
|
generatedAt: '2026-07-20T01:02:03.000Z',
|
|
config: {
|
|
enabled: true,
|
|
websiteId: 'local-website',
|
|
idSecret: 'secret',
|
|
scriptPath: '/analytics/script.js',
|
|
hostPath: '/analytics',
|
|
domains: '127.0.0.1,localhost',
|
|
identityMode: 'raw',
|
|
},
|
|
});
|
|
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,plan_type:d\.plan_type,channel:d\.channel,surface:d\.surface,identity_mode:d\.identity_mode\}\)/);
|
|
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.match(out, /"username":"张三"/);
|
|
assert.match(out, /page_click/);
|
|
assert.match(out, /target_path/);
|
|
assert.doesNotMatch(out, /target_url/);
|
|
assert.match(out, /page_form_submit/);
|
|
assert.match(out, /page_scroll_/);
|
|
assert.match(out, /page_engaged_10s/);
|
|
assert.match(out, /"owner_id":"user-123"/);
|
|
assert.match(out, /"generated_at":"2026-07-20T01:02:03.000Z"/);
|
|
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('<!doctype html><html><head></head><body></body></html>', {
|
|
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(/<script data-memind-analytics="1">([\s\S]*?)<\/script>/)?.[1];
|
|
assert.ok(inlineScript);
|
|
|
|
const calls = [];
|
|
vm.runInNewContext(inlineScript, {
|
|
window: {
|
|
umami: {
|
|
identify: (...args) => calls.push(['identify', ...args]),
|
|
track: (...args) => calls.push(['track', ...args]),
|
|
},
|
|
innerHeight: 800,
|
|
scrollY: 0,
|
|
addEventListener: () => {},
|
|
},
|
|
document: {
|
|
readyState: 'complete',
|
|
title: 'Demo',
|
|
documentElement: { scrollHeight: 1600 },
|
|
addEventListener: () => {},
|
|
},
|
|
location: { href: 'https://m.tkmind.cn/MindSpace/demo/public/page.html' },
|
|
setTimeout: () => {},
|
|
});
|
|
|
|
assert.deepEqual(JSON.parse(JSON.stringify(calls)), [
|
|
['identify', pseudonymizeAnalyticsId('user-123', 'secret'), {
|
|
username: '张三',
|
|
memind_page_url: 'https://m.tkmind.cn/MindSpace/demo/public/page.html',
|
|
owner_segment: 'plan:pro',
|
|
channel: 'h5',
|
|
surface: 'generated_page',
|
|
plan_type: 'unknown',
|
|
identity_mode: 'pseudonymous',
|
|
}],
|
|
['track'],
|
|
]);
|
|
});
|
|
|
|
test('does not alter non-full-html or disabled pages', () => {
|
|
const fragment = '<div>hello</div>';
|
|
assert.equal(injectMindSpaceAnalytics(fragment, { ownerId: 'u', config: { enabled: true, websiteId: 'w', idSecret: 's' } }), fragment);
|
|
const html = '<!doctype html><html><head></head><body></body></html>';
|
|
assert.equal(injectMindSpaceAnalytics(html, { ownerId: 'u', config: { enabled: false, websiteId: 'w', idSecret: 's' } }), html);
|
|
});
|
|
|
|
test('analytics event sender is fail-open when analytics is disabled', async () => {
|
|
assert.equal(await sendMindSpaceAnalyticsEvent({ eventName: 'page_generated', ownerId: 'u', config: { enabled: false } }), false);
|
|
});
|
|
|
|
test('generation events are attributed to raw identity and include analysis dimensions', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let requestBody;
|
|
globalThis.fetch = async (_url, init) => {
|
|
requestBody = JSON.parse(init.body);
|
|
return { ok: true };
|
|
};
|
|
try {
|
|
assert.equal(await sendMindSpaceAnalyticsEvent({
|
|
config: {
|
|
enabled: true,
|
|
websiteId: 'generated-pages',
|
|
idSecret: 'secret',
|
|
identityMode: 'raw',
|
|
analyticsUrl: 'http://127.0.0.1:3100',
|
|
},
|
|
eventName: 'page_generated',
|
|
ownerId: 'user-123',
|
|
ownerLabel: '张三',
|
|
ownerSegment: 'plan:pro',
|
|
planType: 'pro',
|
|
pageId: 'page-123',
|
|
generatedAt: '2026-07-20T01:02:03.000Z',
|
|
}), true);
|
|
assert.equal(requestBody.payload.id, 'user-123');
|
|
assert.deepEqual(requestBody.payload.data, expectPayload({
|
|
owner_id: 'user-123',
|
|
owner_label: '张三',
|
|
owner_segment: 'plan:pro',
|
|
plan_type: 'pro',
|
|
page_id: 'page-123',
|
|
generated_at: '2026-07-20T01:02:03.000Z',
|
|
identity_mode: 'raw',
|
|
}));
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
function expectPayload(expected) {
|
|
return Object.assign({
|
|
publication_id: '',
|
|
agent_run_id: '',
|
|
channel: 'h5',
|
|
}, expected);
|
|
}
|