115 lines
6.0 KiB
JavaScript
115 lines
6.0 KiB
JavaScript
import crypto from 'node:crypto';
|
|
|
|
import {
|
|
pseudonymizeAnalyticsId,
|
|
resolveAnalyticsOwnerLabel,
|
|
resolveAnalyticsOwnerSegment,
|
|
} from './mindspace-analytics.mjs';
|
|
|
|
const RYBBIT_MARKER = 'data-memind-rybbit="1"';
|
|
|
|
export function resolveMindSpaceRybbitConfig(env = process.env) {
|
|
const enabled = String(env.MEMIND_RYBBIT_ENABLED ?? '').toLowerCase() === 'true';
|
|
const siteId = String(env.MEMIND_RYBBIT_SITE_ID ?? '').trim();
|
|
const secret = String(env.MEMIND_RYBBIT_ID_SECRET ?? env.MEMIND_ANALYTICS_ID_SECRET ?? '').trim();
|
|
const rybbitUrl = String(env.MEMIND_RYBBIT_URL ?? env.RYBBIT_URL ?? 'https://rybbit.tkmind.cn').trim()
|
|
|| 'https://rybbit.tkmind.cn';
|
|
return {
|
|
enabled: enabled && Boolean(siteId) && Boolean(secret),
|
|
siteId,
|
|
idSecret: secret,
|
|
rybbitUrl: rybbitUrl.replace(/\/$/, ''),
|
|
// Same-origin proxy path. Script auto-discovers host by splitting on /script.js,
|
|
// so /rybbit/script.js makes events go to /rybbit/track → proxied to /api/track.
|
|
scriptPath: String(env.MEMIND_RYBBIT_SCRIPT_PATH ?? '/rybbit/script.js').trim() || '/rybbit/script.js',
|
|
hostPath: String(env.MEMIND_RYBBIT_HOST_PATH ?? '/rybbit').trim() || '/rybbit',
|
|
};
|
|
}
|
|
|
|
export { resolveAnalyticsOwnerLabel, resolveAnalyticsOwnerSegment, pseudonymizeAnalyticsId };
|
|
|
|
function jsonForInlineScript(value) {
|
|
return JSON.stringify(value)
|
|
.replaceAll('<', '\\u003c')
|
|
.replaceAll('>', '\\u003e')
|
|
.replaceAll('&', '\\u0026');
|
|
}
|
|
|
|
export function sendMindSpaceRybbitEvent({
|
|
config,
|
|
eventName,
|
|
ownerId,
|
|
pageId = '',
|
|
publicationId = '',
|
|
agentRunId = '',
|
|
channel = 'h5',
|
|
ownerSegment = 'unknown',
|
|
ownerLabel = '未命名用户',
|
|
url = '',
|
|
} = {}) {
|
|
if (!config?.enabled || !config.siteId || !config.idSecret || !eventName) return Promise.resolve(false);
|
|
const owner = pseudonymizeAnalyticsId(ownerId, config.idSecret);
|
|
if (!owner) return Promise.resolve(false);
|
|
const endpoint = `${String(config.rybbitUrl || 'https://rybbit.tkmind.cn').replace(/\/$/, '')}/api/track`;
|
|
const payload = {
|
|
site_id: String(config.siteId),
|
|
type: 'custom_event',
|
|
event_name: String(eventName).slice(0, 256),
|
|
pathname: url || '/',
|
|
page_title: '',
|
|
referrer: '',
|
|
hostname: '127.0.0.1',
|
|
user_id: owner,
|
|
properties: JSON.stringify({
|
|
owner_id: owner,
|
|
page_id: String(pageId || ''),
|
|
publication_id: String(publicationId || ''),
|
|
agent_run_id: String(agentRunId || ''),
|
|
channel,
|
|
owner_segment: String(ownerSegment || 'unknown'),
|
|
owner_label: resolveAnalyticsOwnerLabel({ displayName: ownerLabel }),
|
|
}),
|
|
};
|
|
return fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json', 'user-agent': 'Memind/local-rybbit' },
|
|
body: JSON.stringify(payload),
|
|
signal: AbortSignal.timeout(1500),
|
|
}).then((response) => response.ok).catch(() => false);
|
|
}
|
|
|
|
export function injectMindSpaceRybbit(html, {
|
|
ownerId,
|
|
pageId = '',
|
|
publicationId = '',
|
|
ownerSegment = 'unknown',
|
|
ownerLabel = '未命名用户',
|
|
channel = 'h5',
|
|
config = resolveMindSpaceRybbitConfig(),
|
|
} = {}) {
|
|
const source = String(html ?? '');
|
|
if (!config?.enabled || !config.siteId || !/^\s*(<!doctype html|<html\b)/i.test(source)) return source;
|
|
if (source.includes(RYBBIT_MARKER)) return source;
|
|
const owner = pseudonymizeAnalyticsId(ownerId, config.idSecret);
|
|
if (!owner) return source;
|
|
|
|
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 = [
|
|
RYBBIT_MARKER,
|
|
`data-site-id="${String(config.siteId).replaceAll('"', '"')}"`,
|
|
'defer',
|
|
`src="${config.scriptPath}"`,
|
|
];
|
|
// Manual identify + engagement events; keep initial pageview from Rybbit site settings.
|
|
const block = `<script ${attrs.join(' ')}></script><script ${RYBBIT_MARKER}>(function(){var d=${jsonForInlineScript(metadata)},seen={},tries=0;function api(){return window.rybbit;}function t(n,x){var r=api();if(!r||typeof r.event!=='function')return;var p=Object.assign({},d,{page_url:location.href,page_title:document.title},x||{});r.event(n,p);}function identify(){var r=api();if(!r||typeof r.identify!=='function')return;r.identify(d.owner_id,{username:d.username,memind_page_url:location.href,owner_segment:d.owner_segment,channel:d.channel,page_id:d.page_id,publication_id:d.publication_id});}function once(n,x){if(seen[n])return;seen[n]=1;t(n,x);}function wire(){identify();document.addEventListener('click',function(e){var el=e.target&&e.target.closest?e.target.closest('a,button,[role="button"],[data-rybbit-event],[data-umami-event]'):null;if(!el)return;var custom=el.getAttribute('data-rybbit-event')||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_url:href.slice(0,500)});},{passive:true});document.addEventListener('submit',function(e){var form=e.target;t('page_form_submit',{form_id:form&&form.id||'',form_action: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();}function ready(){if(api()){wire();return;}if(tries++>40)return;setTimeout(ready,50);}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`);
|
|
}
|