feat: add attributable page analytics

This commit is contained in:
john
2026-07-20 20:49:44 +08:00
parent d479e89fc7
commit 1c82b34ad9
12 changed files with 144 additions and 11 deletions
+1
View File
@@ -18,6 +18,7 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
# MEMIND_ANALYTICS_URL=http://127.0.0.1:3100
# MEMIND_ANALYTICS_WEBSITE_ID=<local Umami Website ID>
# MEMIND_ANALYTICS_ID_SECRET=<local-only pseudonymization secret>
# MEMIND_ANALYTICS_IDENTITY_MODE=pseudonymous # use raw only in an approved first-party analytics environment
# MEMIND_ANALYTICS_DOMAINS=127.0.0.1,localhost
# Memind 产品壳层使用独立 Website,避免与用户生成页面的浏览口径混在一起
# MEMIND_PRODUCT_ANALYTICS_ENABLED=true
+6 -1
View File
@@ -10,9 +10,14 @@ MEMIND_ANALYTICS_ENABLED=true
MEMIND_ANALYTICS_URL=http://127.0.0.1:3100
MEMIND_ANALYTICS_WEBSITE_ID=<website-id>
MEMIND_ANALYTICS_ID_SECRET=<random-local-secret>
MEMIND_ANALYTICS_IDENTITY_MODE=raw
MEMIND_ANALYTICS_DOMAINS=127.0.0.1,localhost
```
`MEMIND_ANALYTICS_IDENTITY_MODE=raw` sends the stable Memind user ID to an
explicitly approved first-party analytics service. Omit it elsewhere to keep the
pseudonymous default.
Create a second Umami Website for the Memind product shell. Keeping it separate
prevents chat, MindSpace, and feedback navigation from inflating generated-page
views. Do not create a Website per page or per user.
@@ -27,7 +32,7 @@ The product shell loads its tracker from the same-origin
transition, `product_click` for links/buttons, and 10/30-second route engagement
events. Query strings are restricted to a small allowlist and click labels never
copy arbitrary chat or generated-page text. Authenticated product events use the
same pseudonymous identity as generated pages while remaining in the separate
same configured identity as generated pages while remaining in the separate
product Website.
## Rybbit (recommended for behavior analytics)
+32 -8
View File
@@ -10,6 +10,9 @@ export function resolveMindSpaceAnalyticsConfig(env = process.env) {
enabled: enabled && Boolean(websiteId) && Boolean(secret),
websiteId,
idSecret: secret,
identityMode: String(env.MEMIND_ANALYTICS_IDENTITY_MODE ?? 'pseudonymous').trim().toLowerCase() === 'raw'
? 'raw'
: 'pseudonymous',
analyticsUrl: String(env.MEMIND_ANALYTICS_URL ?? 'http://127.0.0.1:3100').trim() || 'http://127.0.0.1:3100',
scriptPath: String(env.MEMIND_ANALYTICS_SCRIPT_PATH ?? '/analytics/script.js').trim() || '/analytics/script.js',
hostPath: String(env.MEMIND_ANALYTICS_HOST_PATH ?? '/analytics').trim() || '/analytics',
@@ -27,6 +30,7 @@ export function resolveProductAnalyticsConfig(
enabled: enabled && Boolean(websiteId) && Boolean(baseConfig?.idSecret),
websiteId,
idSecret: String(baseConfig?.idSecret ?? '').trim(),
identityMode: baseConfig?.identityMode === 'raw' ? 'raw' : 'pseudonymous',
scriptPath: String(baseConfig?.scriptPath ?? '/analytics/script.js').trim() || '/analytics/script.js',
hostPath: String(baseConfig?.hostPath ?? '/analytics').trim() || '/analytics',
domains: String(baseConfig?.domains ?? '').trim(),
@@ -40,12 +44,25 @@ export function pseudonymizeAnalyticsId(value, secret) {
return crypto.createHmac('sha256', key).update(normalized).digest('hex').slice(0, 32);
}
export function resolveAnalyticsIdentity(value, config = {}) {
const normalized = String(value ?? '').trim().replace(/[\r\n\t]+/g, '').slice(0, 128);
if (!normalized) return '';
return config?.identityMode === 'raw'
? normalized
: pseudonymizeAnalyticsId(normalized, config?.idSecret);
}
export function resolveAnalyticsOwnerSegment(user = {}) {
if (user?.role === 'admin') return 'admin';
const plan = String(user?.planType ?? user?.plan_type ?? 'free').trim().toLowerCase();
return `plan:${plan || 'free'}`;
}
export function resolveAnalyticsPlan(user = {}) {
if (user?.role === 'admin') return 'admin';
return String(user?.planType ?? user?.plan_type ?? 'free').trim().toLowerCase() || 'free';
}
export function resolveAnalyticsOwnerLabel(user = {}) {
const label = String(user?.displayName ?? user?.display_name ?? user?.username ?? '').trim();
return label.replace(/[\r\n\t]+/g, ' ').slice(0, 80) || '未命名用户';
@@ -53,7 +70,7 @@ export function resolveAnalyticsOwnerLabel(user = {}) {
export function buildProductAnalyticsContext({ config, user = null } = {}) {
if (!config?.enabled || !config.websiteId) return { enabled: false };
const distinctId = user?.id ? pseudonymizeAnalyticsId(user.id, config.idSecret) : '';
const distinctId = user?.id ? resolveAnalyticsIdentity(user.id, config) : '';
return {
enabled: true,
websiteId: config.websiteId,
@@ -65,8 +82,10 @@ export function buildProductAnalyticsContext({ config, user = null } = {}) {
distinctId,
username: resolveAnalyticsOwnerLabel(user),
ownerSegment: resolveAnalyticsOwnerSegment(user),
planType: resolveAnalyticsPlan(user),
channel: 'h5',
surface: 'product',
identityMode: config.identityMode === 'raw' ? 'raw' : 'pseudonymous',
}
: null,
};
@@ -82,14 +101,17 @@ export function sendMindSpaceAnalyticsEvent({
channel = 'h5',
ownerSegment = 'unknown',
ownerLabel = '未命名用户',
planType = 'unknown',
generatedAt = '',
url = '',
} = {}) {
if (!config?.enabled || !config.websiteId || !config.idSecret || !eventName) return Promise.resolve(false);
const owner = pseudonymizeAnalyticsId(ownerId, config.idSecret);
const owner = resolveAnalyticsIdentity(ownerId, config);
if (!owner) return Promise.resolve(false);
const endpoint = `${String(config.analyticsUrl || 'http://127.0.0.1:3100').replace(/\/$/, '')}/api/send`;
const payload = {
website: config.websiteId,
id: owner,
hostname: '127.0.0.1',
url: url || '/',
name: String(eventName),
@@ -100,7 +122,10 @@ export function sendMindSpaceAnalyticsEvent({
agent_run_id: String(agentRunId || ''),
channel,
owner_segment: String(ownerSegment || 'unknown'),
plan_type: String(planType || 'unknown'),
owner_label: resolveAnalyticsOwnerLabel({ displayName: ownerLabel }),
generated_at: String(generatedAt || ''),
identity_mode: config.identityMode === 'raw' ? 'raw' : 'pseudonymous',
},
};
return fetch(endpoint, {
@@ -124,18 +149,17 @@ export function injectMindSpaceAnalytics(html, {
publicationId = '',
ownerSegment = 'unknown',
ownerLabel = '未命名用户',
planType = 'unknown',
generatedAt = '',
channel = 'h5',
config = resolveMindSpaceAnalyticsConfig(),
} = {}) {
const source = String(html ?? '');
if (!config?.enabled || !config.websiteId || !/^\s*(<!doctype html|<html\b)/i.test(source)) return source;
if (source.includes(ANALYTICS_MARKER)) return source;
const owner = pseudonymizeAnalyticsId(ownerId, config.idSecret);
const owner = resolveAnalyticsIdentity(ownerId, config);
if (!owner) return source;
// 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, surface: 'generated_page' };
const metadata = { owner_id: owner, username: resolveAnalyticsOwnerLabel({ displayName: ownerLabel }), owner_segment: String(ownerSegment || 'unknown'), plan_type: String(planType || 'unknown'), page_id: String(pageId || ''), publication_id: String(publicationId || ''), generated_at: String(generatedAt || ''), identity_mode: config.identityMode === 'raw' ? 'raw' : 'pseudonymous', channel, surface: 'generated_page' };
const attrs = [
ANALYTICS_MARKER,
`data-website-id="${config.websiteId.replaceAll('"', '&quot;')}"`,
@@ -143,7 +167,7 @@ export function injectMindSpaceAnalytics(html, {
`data-host-url="${config.hostPath}"`,
];
if (config.domains) attrs.push(`data-domains="${config.domains.replaceAll('"', '&quot;')}"`);
const block = `<script ${attrs.join(' ')} defer src="${config.scriptPath}"></script><script ${ANALYTICS_MARKER}>(function(){var d=${jsonForInlineScript(metadata)},seen={};function safeTarget(h){if(!h)return'';try{var u=new URL(h,location.href);return u.origin===location.origin?u.pathname:'external:'+u.hostname;}catch{return'';}}function t(n,x){if(!window.umami||typeof window.umami.track!=='function')return;var p=Object.assign({},d,{page_url:location.href,page_route:location.pathname,page_title:document.title},x||{});window.umami.track(n,p);}function identify(){if(!window.umami||typeof window.umami.identify!=='function')return;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});}function pageview(){if(!window.umami||typeof window.umami.track!=='function')return;window.umami.track();}function once(n,x){if(seen[n])return;seen[n]=1;t(n,x);}function ready(){identify();pageview();document.addEventListener('click',function(e){var el=e.target&&e.target.closest?e.target.closest('a,button,[role="button"],[data-umami-event]'):null;if(!el)return;var custom=el.getAttribute('data-umami-event');var href=el.tagName==='A'?el.getAttribute('href')||'':'';t(custom||'page_click',{element:el.tagName.toLowerCase(),element_id:el.id||'',event_label:(custom||el.getAttribute('aria-label')||'').slice(0,100),target_path:safeTarget(href)});},{passive:true});document.addEventListener('submit',function(e){var form=e.target;t('page_form_submit',{form_id:form&&form.id||'',form_action:safeTarget(form&&form.getAttribute('action')||'')});},{passive:true});var marks=[25,50,75,90];function scroll(){var h=document.documentElement.scrollHeight-window.innerHeight;if(h<=0){once('page_scroll_100');return;}var pct=Math.round(window.scrollY/h*100);marks.forEach(function(m){if(pct>=m)once('page_scroll_'+m);});}window.addEventListener('scroll',scroll,{passive:true});setTimeout(function(){once('page_engaged_10s');},10000);setTimeout(function(){once('page_engaged_30s');},30000);scroll();}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',ready,{once:true});}else{ready();}})();</script>`;
const block = `<script ${attrs.join(' ')} defer src="${config.scriptPath}"></script><script ${ANALYTICS_MARKER}>(function(){var d=${jsonForInlineScript(metadata)},seen={};function safeTarget(h){if(!h)return'';try{var u=new URL(h,location.href);return u.origin===location.origin?u.pathname:'external:'+u.hostname;}catch{return'';}}function t(n,x){if(!window.umami||typeof window.umami.track!=='function')return;var p=Object.assign({},d,{page_url:location.href,page_route:location.pathname,page_title:document.title},x||{});window.umami.track(n,p);}function identify(){if(!window.umami||typeof window.umami.identify!=='function')return;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});}function pageview(){if(!window.umami||typeof window.umami.track!=='function')return;window.umami.track();}function once(n,x){if(seen[n])return;seen[n]=1;t(n,x);}function ready(){identify();pageview();document.addEventListener('click',function(e){var el=e.target&&e.target.closest?e.target.closest('a,button,[role="button"],[data-umami-event]'):null;if(!el)return;var custom=el.getAttribute('data-umami-event');var href=el.tagName==='A'?el.getAttribute('href')||'':'';t(custom||'page_click',{element:el.tagName.toLowerCase(),element_id:el.id||'',event_label:(custom||el.getAttribute('aria-label')||'').slice(0,100),target_path:safeTarget(href)});},{passive:true});document.addEventListener('submit',function(e){var form=e.target;t('page_form_submit',{form_id:form&&form.id||'',form_action:safeTarget(form&&form.getAttribute('action')||'')});},{passive:true});var marks=[25,50,75,90];function scroll(){var h=document.documentElement.scrollHeight-window.innerHeight;if(h<=0){once('page_scroll_100');return;}var pct=Math.round(window.scrollY/h*100);marks.forEach(function(m){if(pct>=m)once('page_scroll_'+m);});}window.addEventListener('scroll',scroll,{passive:true});setTimeout(function(){once('page_engaged_10s');},10000);setTimeout(function(){once('page_engaged_30s');},30000);scroll();}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',ready,{once:true});}else{ready();}})();</script>`;
if (/<\/head>/i.test(source)) return source.replace(/<\/head>/i, `${block}</head>`);
return source.replace(/<body\b/i, `${block}<body`);
}
+67 -2
View File
@@ -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);
}
+3
View File
@@ -150,6 +150,9 @@ export async function resolveMindSpacePageDataContext({
pageId: page.id,
accessMode: page.publicationAccessMode ?? null,
publicationId: page.currentPublishId ?? page.current_publish_id ?? null,
...(page.createdAt
? { generatedAt: new Date(page.createdAt).toISOString() }
: {}),
};
}
+2
View File
@@ -98,6 +98,7 @@ test('resolveMindSpacePageDataContext falls back to the MindSpace page service',
id: 'remote-page',
publicationAccessMode: 'internal',
currentPublishId: 'remote-publish',
createdAt: '2026-07-20T12:34:56.000Z',
};
},
},
@@ -108,5 +109,6 @@ test('resolveMindSpacePageDataContext falls back to the MindSpace page service',
pageId: 'remote-page',
accessMode: 'internal',
publicationId: 'remote-publish',
generatedAt: '2026-07-20T12:34:56.000Z',
});
});
@@ -1,6 +1,7 @@
import {
resolveAnalyticsOwnerLabel,
resolveAnalyticsOwnerSegment,
resolveAnalyticsPlan,
sendMindSpaceAnalyticsEvent,
} from '../mindspace-analytics.mjs';
import {
@@ -43,6 +44,7 @@ export async function bootstrapPortalIntegrationServices({
resolveAnalyticsOwnerSegment,
resolveAnalyticsOwnerLabelFn =
resolveAnalyticsOwnerLabel,
resolveAnalyticsPlanFn = resolveAnalyticsPlan,
sendMindSpaceAnalyticsEventFn =
sendMindSpaceAnalyticsEvent,
sendMindSpaceRybbitEventFn =
@@ -155,6 +157,8 @@ export async function bootstrapPortalIntegrationServices({
ownerLabel: resolveAnalyticsOwnerLabelFn(
pageOwner,
),
planType: resolveAnalyticsPlanFn(pageOwner),
generatedAt: new Date().toISOString(),
pageId: artifact.relativePath,
publicationId: sessionId,
agentRunId: sessionId,
@@ -148,6 +148,10 @@ function createSetup(overrides = {}) {
calls.push(['owner-label', user]);
return 'label';
},
resolveAnalyticsPlanFn(user) {
calls.push(['plan-type', user]);
return 'pro';
},
sendMindSpaceAnalyticsEventFn(event) {
calls.push(['analytics', event]);
return Promise.resolve();
@@ -349,12 +353,18 @@ test('preserves generated-page analytics projection', async () => {
ownerId: 'user-1',
ownerSegment: 'segment',
ownerLabel: 'label',
planType: 'pro',
generatedAt: analyticsCall[1].generatedAt,
pageId: 'public/page.html',
publicationId: 'session-1',
agentRunId: 'session-1',
channel: 'wechat_mp',
url: 'https://example/page',
});
assert.match(
analyticsCall[1].generatedAt,
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
);
const rybbitCall = setup.calls.find(
([name]) => name === 'rybbit',
);
+4
View File
@@ -9,6 +9,7 @@ import {
import {
resolveAnalyticsOwnerLabel,
resolveAnalyticsOwnerSegment,
resolveAnalyticsPlan,
sendMindSpaceAnalyticsEvent,
} from '../mindspace-analytics.mjs';
import {
@@ -96,6 +97,7 @@ export function attachPortalSessionRoutes(
resolveAnalyticsOwnerSegment,
resolveAnalyticsOwnerLabelFn =
resolveAnalyticsOwnerLabel,
resolveAnalyticsPlanFn = resolveAnalyticsPlan,
syncPublicHtmlAfterFinishFn = syncPublicHtmlAfterFinish,
resolveMindSpaceRuntimeConfigFn =
resolveMindSpaceRuntimeConfig,
@@ -432,6 +434,8 @@ export function attachPortalSessionRoutes(
resolveAnalyticsOwnerSegmentFn(req.currentUser),
ownerLabel:
resolveAnalyticsOwnerLabelFn(req.currentUser),
planType: resolveAnalyticsPlanFn(req.currentUser),
generatedAt: new Date().toISOString(),
pageId: relativePath,
publicationId: sessionId,
agentRunId: sessionId,
+6
View File
@@ -165,6 +165,7 @@ function createDependencies(overrides = {}) {
sendMindSpaceAnalyticsEventFn: async () => {},
resolveAnalyticsOwnerSegmentFn: () => 'segment',
resolveAnalyticsOwnerLabelFn: () => 'label',
resolveAnalyticsPlanFn: () => 'pro',
syncPublicHtmlAfterFinishFn: async () => ({
publicHtmlRelativePaths: [],
}),
@@ -524,6 +525,11 @@ test('stream hook uses current session id for contracts and analytics', async ()
assert.equal(calls[1].kind, 'materialize');
assert.equal(calls[2].input.publicationId, 'session-1');
assert.equal(calls[2].input.agentRunId, 'session-1');
assert.equal(calls[2].input.planType, 'pro');
assert.match(
calls[2].input.generatedAt,
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
);
});
test('Finish hook preserves refresh, sync, delivery readiness, memory, and lock lifecycle', async () => {
@@ -7,6 +7,7 @@ import {
injectMindSpaceAnalytics,
resolveAnalyticsOwnerLabel,
resolveAnalyticsOwnerSegment,
resolveAnalyticsPlan,
} from '../mindspace-analytics.mjs';
import {
injectMindSpaceRybbit,
@@ -303,6 +304,8 @@ export function createPortalWorkspacePublicationDelivery({
ownerLabel: resolveAnalyticsOwnerLabel(
pageOwner ?? {},
),
planType: resolveAnalyticsPlan(pageOwner ?? {}),
generatedAt: pageDataContext?.generatedAt ?? '',
pageId:
pageDataContext?.pageId ?? '',
publicationId:
+6
View File
@@ -5,8 +5,10 @@ type AnalyticsIdentity = {
distinctId: string;
username: string;
ownerSegment: string;
planType: string;
channel: string;
surface: 'product';
identityMode: 'raw' | 'pseudonymous';
};
type ProductAnalyticsContext = {
@@ -160,6 +162,8 @@ function trackProductEvent(
route_name: resolveProductRouteName(window.location.pathname),
channel: identity?.channel || 'h5',
owner_segment: identity?.ownerSegment || 'anonymous',
plan_type: identity?.planType || 'anonymous',
identity_mode: identity?.identityMode || 'anonymous',
...data,
},
}));
@@ -203,8 +207,10 @@ export function useProductAnalytics(userId?: string | null) {
void window.umami?.identify(identity.distinctId, {
username: identity.username,
owner_segment: identity.ownerSegment,
plan_type: identity.planType,
channel: identity.channel,
surface: identity.surface,
identity_mode: identity.identityMode,
});
}
} else if (lastIdentityKey) {