diff --git a/docs/local-analytics.md b/docs/local-analytics.md index af850d2..7cf1b33 100644 --- a/docs/local-analytics.md +++ b/docs/local-analytics.md @@ -58,8 +58,14 @@ Generated Page MEMIND_RYBBIT_ID_SECRET= ``` + If `MEMIND_RYBBIT_ID_SECRET` is omitted, Portal bootstrap reuses the + MindSpace analytics secret stored in `mindspace_config` (the same value + configured below in memind_adm → Analytics 配置). + 3. Restart the local Memind server. Full generated HTML pages receive a - same-origin `/rybbit/script.js` tracker. The tracker identifies the visitor + same-origin `/rybbit/script.js` tracker. Publication routes under + `/u/:owner/pages/:slug` and `/MindSpace/:user/public/...` both receive + the tracker. The tracker identifies the visitor with a stable pseudonymous owner ID, then records engagement events such as `page_click`, scroll depth, and dwell time. Initial pageviews come from Rybbit's site setting `trackInitialPageView`. diff --git a/mindspace-rybbit.mjs b/mindspace-rybbit.mjs index 6b6f2de..4d77aa9 100644 --- a/mindspace-rybbit.mjs +++ b/mindspace-rybbit.mjs @@ -8,10 +8,18 @@ import { const RYBBIT_MARKER = 'data-memind-rybbit="1"'; -export function resolveMindSpaceRybbitConfig(env = process.env) { +export function resolveMindSpaceRybbitConfig( + env = process.env, + { idSecret = '' } = {}, +) { 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 secret = String( + env.MEMIND_RYBBIT_ID_SECRET ?? + env.MEMIND_ANALYTICS_ID_SECRET ?? + idSecret ?? + '', + ).trim(); const rybbitUrl = String(env.MEMIND_RYBBIT_URL ?? env.RYBBIT_URL ?? 'https://rybbit.tkmind.cn').trim() || 'https://rybbit.tkmind.cn'; return { diff --git a/mindspace-rybbit.test.mjs b/mindspace-rybbit.test.mjs index fed0f98..58bc039 100644 --- a/mindspace-rybbit.test.mjs +++ b/mindspace-rybbit.test.mjs @@ -26,6 +26,17 @@ test('rybbit config is disabled unless explicitly enabled and configured', () => assert.equal(config.scriptPath, '/rybbit/script.js'); }); +test('rybbit config can reuse stored analytics secret', () => { + const config = resolveMindSpaceRybbitConfig({ + MEMIND_RYBBIT_ENABLED: 'true', + MEMIND_RYBBIT_SITE_ID: '2', + }, { + idSecret: 'stored-secret', + }); + assert.equal(config.enabled, true); + assert.equal(config.idSecret, 'stored-secret'); +}); + test('injects one local same-origin rybbit tracker with page dimensions', () => { const html = 'Demo

Demo

'; const out = injectMindSpaceRybbit(html, { diff --git a/server.mjs b/server.mjs index a59fe90..81b01f1 100644 --- a/server.mjs +++ b/server.mjs @@ -348,8 +348,15 @@ async function bootstrapUserAuth() { mindSpaceRuntimeAdapter, resolveUserIdByDirKey, } = domainServices; - mindSpaceAnalyticsConfig = - domainServices.mindSpaceAnalyticsConfig; + Object.assign( + mindSpaceAnalyticsConfig, + domainServices.mindSpaceAnalyticsConfig, + ); + Object.assign( + mindSpaceRybbitConfig, + domainServices.mindSpaceRybbitConfig ?? + mindSpaceRybbitConfig, + ); scheduleService = domainServices.scheduleService; pageDataService = domainServices.pageDataService; pageDataPublicService = @@ -1490,6 +1497,11 @@ const sendPublishedPage = createPortalPublishedPageDelivery({ registerLongImageArtifact: registerPublishedLongImageArtifactForConversation, + analyticsConfig: mindSpaceAnalyticsConfig, + rybbitConfig: mindSpaceRybbitConfig, + getAuthPool: () => authPool, + getUserAuth: () => userAuth, + getMindSpacePages: () => mindSpacePages, }); attachPortalPublicationRoutes({ diff --git a/server/portal-domain-services-bootstrap.mjs b/server/portal-domain-services-bootstrap.mjs index de97459..84367e2 100644 --- a/server/portal-domain-services-bootstrap.mjs +++ b/server/portal-domain-services-bootstrap.mjs @@ -1,4 +1,5 @@ import { ensureMindSpaceConfig, loadMindSpaceConfig } from '../mindspace-config.mjs'; +import { resolveMindSpaceRybbitConfig } from '../mindspace-rybbit.mjs'; import { createMindSearchConfigService } from '../mindsearch-config.mjs'; import { createMindSpaceService } from '../mindspace.mjs'; import { @@ -105,6 +106,9 @@ export async function bootstrapPortalDomainServices({ scriptPath: '/analytics/script.js', }; } + const resolvedRybbitConfig = resolveMindSpaceRybbitConfig(env, { + idSecret: storedMindSpaceConfig?.analytics?.idSecret ?? '', + }); const scheduleService = createScheduleServiceFn(pool, { defaultTimezone: @@ -272,6 +276,7 @@ export async function bootstrapPortalDomainServices({ return { mindSearchConfigService, mindSpaceAnalyticsConfig: resolvedAnalyticsConfig, + mindSpaceRybbitConfig: resolvedRybbitConfig, scheduleService, pageDataService, pageDataPublicService, diff --git a/server/portal-domain-services-bootstrap.test.mjs b/server/portal-domain-services-bootstrap.test.mjs index 8e39196..b745a81 100644 --- a/server/portal-domain-services-bootstrap.test.mjs +++ b/server/portal-domain-services-bootstrap.test.mjs @@ -61,6 +61,8 @@ function createBootstrapSetup(overrides = {}) { env: { H5_DEFAULT_TIMEZONE: 'Asia/Singapore', PLAZA_REDIS_URL: 'redis://plaza', + MEMIND_RYBBIT_ENABLED: 'true', + MEMIND_RYBBIT_SITE_ID: '2', }, runtime: { maxFileBytes: 100, @@ -309,6 +311,9 @@ test('domain bootstrap preserves schema and service assembly order', async () => result.mindSpaceAnalyticsConfig.scriptPath, '/analytics/script.js', ); + assert.equal(result.mindSpaceRybbitConfig.enabled, true); + assert.equal(result.mindSpaceRybbitConfig.siteId, '2'); + assert.equal(result.mindSpaceRybbitConfig.idSecret, 'secret-1'); }); test('domain bootstrap preserves delayed user, snapshot, and Page Data dependencies', async () => { diff --git a/server/portal-publication-shell.mjs b/server/portal-publication-shell.mjs index b9be057..29e5344 100644 --- a/server/portal-publication-shell.mjs +++ b/server/portal-publication-shell.mjs @@ -64,7 +64,7 @@ export function publishedPageShellHtml({ iframeUrl, shareUrl, title, longImageUr - + ${safeTitle}