174 lines
9.1 KiB
JavaScript
174 lines
9.1 KiB
JavaScript
import crypto from 'node:crypto';
|
|
|
|
const ANALYTICS_MARKER = 'data-memind-analytics="1"';
|
|
|
|
export function resolveMindSpaceAnalyticsConfig(env = process.env) {
|
|
const enabled = String(env.MEMIND_ANALYTICS_ENABLED ?? '').toLowerCase() === 'true';
|
|
const websiteId = String(env.MEMIND_ANALYTICS_WEBSITE_ID ?? '').trim();
|
|
const secret = String(env.MEMIND_ANALYTICS_ID_SECRET ?? '').trim();
|
|
return {
|
|
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',
|
|
domains: String(env.MEMIND_ANALYTICS_DOMAINS ?? '').trim(),
|
|
};
|
|
}
|
|
|
|
export function resolveProductAnalyticsConfig(
|
|
env = process.env,
|
|
baseConfig = resolveMindSpaceAnalyticsConfig(env),
|
|
) {
|
|
const websiteId = String(env.MEMIND_PRODUCT_ANALYTICS_WEBSITE_ID ?? '').trim();
|
|
const enabled = String(env.MEMIND_PRODUCT_ANALYTICS_ENABLED ?? '').toLowerCase() === 'true';
|
|
return {
|
|
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(),
|
|
};
|
|
}
|
|
|
|
export function pseudonymizeAnalyticsId(value, secret) {
|
|
const normalized = String(value ?? '').trim();
|
|
const key = String(secret ?? '').trim();
|
|
if (!normalized || !key) return '';
|
|
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) || '未命名用户';
|
|
}
|
|
|
|
export function buildProductAnalyticsContext({ config, user = null } = {}) {
|
|
if (!config?.enabled || !config.websiteId) return { enabled: false };
|
|
const distinctId = user?.id ? resolveAnalyticsIdentity(user.id, config) : '';
|
|
return {
|
|
enabled: true,
|
|
websiteId: config.websiteId,
|
|
scriptPath: config.scriptPath || '/analytics/script.js',
|
|
hostPath: config.hostPath || '/analytics',
|
|
domains: config.domains || '',
|
|
identity: distinctId
|
|
? {
|
|
distinctId,
|
|
username: resolveAnalyticsOwnerLabel(user),
|
|
ownerSegment: resolveAnalyticsOwnerSegment(user),
|
|
planType: resolveAnalyticsPlan(user),
|
|
channel: 'h5',
|
|
surface: 'product',
|
|
identityMode: config.identityMode === 'raw' ? 'raw' : 'pseudonymous',
|
|
}
|
|
: null,
|
|
};
|
|
}
|
|
|
|
export function sendMindSpaceAnalyticsEvent({
|
|
config,
|
|
eventName,
|
|
ownerId,
|
|
pageId = '',
|
|
publicationId = '',
|
|
agentRunId = '',
|
|
channel = 'h5',
|
|
ownerSegment = 'unknown',
|
|
ownerLabel = '未命名用户',
|
|
planType = 'unknown',
|
|
generatedAt = '',
|
|
url = '',
|
|
} = {}) {
|
|
if (!config?.enabled || !config.websiteId || !config.idSecret || !eventName) return Promise.resolve(false);
|
|
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),
|
|
data: {
|
|
owner_id: owner,
|
|
page_id: String(pageId || ''),
|
|
publication_id: String(publicationId || ''),
|
|
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, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json', 'user-agent': 'Memind/local-analytics' },
|
|
body: JSON.stringify({ type: 'event', payload }),
|
|
signal: AbortSignal.timeout(1500),
|
|
}).then((response) => response.ok).catch(() => false);
|
|
}
|
|
|
|
function jsonForInlineScript(value) {
|
|
return JSON.stringify(value)
|
|
.replaceAll('<', '\\u003c')
|
|
.replaceAll('>', '\\u003e')
|
|
.replaceAll('&', '\\u0026');
|
|
}
|
|
|
|
export function injectMindSpaceAnalytics(html, {
|
|
ownerId,
|
|
pageId = '',
|
|
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 = resolveAnalyticsIdentity(ownerId, config);
|
|
if (!owner) return source;
|
|
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('"', '"')}"`,
|
|
'data-auto-track="false"',
|
|
`data-host-url="${config.hostPath}"`,
|
|
];
|
|
if (config.domains) attrs.push(`data-domains="${config.domains.replaceAll('"', '"')}"`);
|
|
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`);
|
|
}
|