feat: add attributable page analytics
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
buildProductAnalyticsContext,
|
||||
injectMindSpaceAnalytics,
|
||||
pseudonymizeAnalyticsId,
|
||||
resolveAnalyticsIdentity,
|
||||
resolveAnalyticsPlan,
|
||||
resolveAnalyticsOwnerSegment,
|
||||
resolveAnalyticsOwnerLabel,
|
||||
resolveMindSpaceAnalyticsConfig,
|
||||
@@ -34,8 +36,10 @@ test('product analytics uses a separate website and the existing pseudonymizatio
|
||||
distinctId: pseudonymizeAnalyticsId('user-123', 'local-secret'),
|
||||
username: '张三',
|
||||
ownerSegment: 'plan:pro',
|
||||
planType: 'pro',
|
||||
channel: 'h5',
|
||||
surface: 'product',
|
||||
identityMode: 'pseudonymous',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,10 +83,18 @@ test('owner ids are stable pseudonyms and never expose the source id', () => {
|
||||
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', () => {
|
||||
@@ -97,6 +109,8 @@ test('injects one local same-origin tracker with page dimensions', () => {
|
||||
ownerLabel: '张三',
|
||||
pageId: 'page-1',
|
||||
publicationId: 'pub-1',
|
||||
planType: 'pro',
|
||||
generatedAt: '2026-07-20T01:02:03.000Z',
|
||||
config: {
|
||||
enabled: true,
|
||||
websiteId: 'local-website',
|
||||
@@ -104,12 +118,13 @@ test('injects one local same-origin tracker with page dimensions', () => {
|
||||
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,channel:d\.channel,surface:d\.surface\}\)/);
|
||||
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'\)/);
|
||||
@@ -123,7 +138,8 @@ test('injects one local same-origin tracker with page dimensions', () => {
|
||||
assert.match(out, /page_form_submit/);
|
||||
assert.match(out, /page_scroll_/);
|
||||
assert.match(out, /page_engaged_10s/);
|
||||
assert.doesNotMatch(out, /user-123/);
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -172,6 +188,8 @@ test('identifies the pseudonymous owner before sending a standard page view', ()
|
||||
owner_segment: 'plan:pro',
|
||||
channel: 'h5',
|
||||
surface: 'generated_page',
|
||||
plan_type: 'unknown',
|
||||
identity_mode: 'pseudonymous',
|
||||
}],
|
||||
['track'],
|
||||
]);
|
||||
@@ -187,3 +205,50 @@ test('does not alter non-full-html or disabled pages', () => {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user