fix: inject Rybbit tracking on m.tkmind.cn publication pages
Memind CI / Test, build, and release guards (push) Successful in 1m36s
Memind CI / Test, build, and release guards (push) Successful in 1m36s
Publication routes and share shells were missing the Rybbit tracker, and bootstrap now reuses the stored MindSpace analytics secret when Rybbit env secret is absent. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -58,8 +58,14 @@ Generated Page
|
||||
MEMIND_RYBBIT_ID_SECRET=<random-local-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`.
|
||||
|
||||
+10
-2
@@ -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 {
|
||||
|
||||
@@ -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 = '<!doctype html><html><head><title>Demo</title></head><body><h1>Demo</h1></body></html>';
|
||||
const out = injectMindSpaceRybbit(html, {
|
||||
|
||||
+14
-2
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -64,7 +64,7 @@ export function publishedPageShellHtml({ iframeUrl, shareUrl, title, longImageUr
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self'; script-src 'unsafe-inline'; frame-src 'self'; base-uri 'none'; form-action 'none'">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self'; script-src 'unsafe-inline' 'self'; frame-src 'self'; base-uri 'none'; form-action 'none'">
|
||||
<title>${safeTitle}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import {
|
||||
injectPublishedPageDataContext,
|
||||
resolveMindSpacePageDataContext,
|
||||
} from '../mindspace-public-page-context.mjs';
|
||||
import {
|
||||
collectInlineScriptHashes,
|
||||
} from '../mindspace-public-delivery.mjs';
|
||||
import {
|
||||
injectMindSpaceAnalytics,
|
||||
resolveAnalyticsOwnerLabel,
|
||||
resolveAnalyticsOwnerSegment,
|
||||
resolveAnalyticsPlan,
|
||||
} from '../mindspace-analytics.mjs';
|
||||
import {
|
||||
injectMindSpaceRybbit,
|
||||
} from '../mindspace-rybbit.mjs';
|
||||
import {
|
||||
rewriteBrokenMindSpacePublicImageUrls,
|
||||
rewritePublicationCanonicalAssetUrls,
|
||||
@@ -35,10 +45,97 @@ import {
|
||||
resolveRequestOrigin,
|
||||
} from './portal-publication-shell.mjs';
|
||||
|
||||
async function decoratePublicationHtmlAnalytics(
|
||||
html,
|
||||
{
|
||||
result,
|
||||
analyticsConfig,
|
||||
rybbitConfig,
|
||||
getAuthPool = () => null,
|
||||
getUserAuth = () => null,
|
||||
getMindSpacePages = () => null,
|
||||
resolvePageDataContext =
|
||||
resolveMindSpacePageDataContext,
|
||||
logger = console,
|
||||
} = {},
|
||||
) {
|
||||
const source = String(html ?? '');
|
||||
const isFullHtml =
|
||||
/^\s*<!doctype html/i.test(source) ||
|
||||
/^\s*<html[\s>]/i.test(source);
|
||||
if (!isFullHtml) return source;
|
||||
|
||||
const userAuth = getUserAuth?.();
|
||||
const pageOwner =
|
||||
result?.ownerId && userAuth
|
||||
? await userAuth
|
||||
.getUserById(result.ownerId)
|
||||
.catch(() => null)
|
||||
: null;
|
||||
let pageDataContext = null;
|
||||
const mindSpacePages = getMindSpacePages?.();
|
||||
if (
|
||||
result?.ownerId &&
|
||||
result?.workspaceRelativePath &&
|
||||
mindSpacePages
|
||||
) {
|
||||
pageDataContext = await resolvePageDataContext({
|
||||
pool: getAuthPool?.(),
|
||||
pageService: mindSpacePages,
|
||||
userId: result.ownerId,
|
||||
relativePath: result.workspaceRelativePath,
|
||||
logger,
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
const ownerSegment = resolveAnalyticsOwnerSegment(
|
||||
pageOwner ?? {},
|
||||
);
|
||||
const ownerLabel = resolveAnalyticsOwnerLabel(
|
||||
pageOwner ?? {},
|
||||
);
|
||||
const pageId =
|
||||
pageDataContext?.pageId ??
|
||||
result?.pageSource?.pageId ??
|
||||
'';
|
||||
const publicationId =
|
||||
pageDataContext?.publicationId ??
|
||||
pageDataContext?.publication_id ??
|
||||
result?.publication?.id ??
|
||||
'';
|
||||
|
||||
let decorated = injectMindSpaceAnalytics(source, {
|
||||
ownerId: result?.ownerId ?? '',
|
||||
ownerSegment,
|
||||
ownerLabel,
|
||||
planType: resolveAnalyticsPlan(pageOwner ?? {}),
|
||||
generatedAt: pageDataContext?.generatedAt ?? '',
|
||||
pageId,
|
||||
publicationId,
|
||||
config: analyticsConfig,
|
||||
});
|
||||
decorated = injectMindSpaceRybbit(decorated, {
|
||||
ownerId: result?.ownerId ?? '',
|
||||
ownerSegment,
|
||||
ownerLabel,
|
||||
pageId,
|
||||
publicationId,
|
||||
channel: 'publication',
|
||||
config: rybbitConfig,
|
||||
});
|
||||
return decorated;
|
||||
}
|
||||
|
||||
export function createPortalPublishedPageDelivery({
|
||||
registerLongImageArtifact,
|
||||
renderLongImage =
|
||||
renderLongImageBuffer,
|
||||
analyticsConfig = { enabled: false },
|
||||
rybbitConfig = { enabled: false },
|
||||
getAuthPool = () => null,
|
||||
getUserAuth = () => null,
|
||||
getMindSpacePages = () => null,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
if (
|
||||
typeof registerLongImageArtifact !==
|
||||
@@ -114,6 +211,15 @@ export function createPortalPublishedPageDelivery({
|
||||
publication: result.publication,
|
||||
},
|
||||
);
|
||||
html = await decoratePublicationHtmlAnalytics(html, {
|
||||
result,
|
||||
analyticsConfig,
|
||||
rybbitConfig,
|
||||
getAuthPool,
|
||||
getUserAuth,
|
||||
getMindSpacePages,
|
||||
logger,
|
||||
});
|
||||
if (!embed) {
|
||||
try {
|
||||
html = injectOgTags(html, {
|
||||
@@ -236,6 +342,15 @@ export function createPortalPublishedPageDelivery({
|
||||
} catch {
|
||||
// Keep the share shell usable.
|
||||
}
|
||||
shellHtml = await decoratePublicationHtmlAnalytics(shellHtml, {
|
||||
result,
|
||||
analyticsConfig,
|
||||
rybbitConfig,
|
||||
getAuthPool,
|
||||
getUserAuth,
|
||||
getMindSpacePages,
|
||||
logger,
|
||||
});
|
||||
res.set(
|
||||
'Content-Type',
|
||||
'text/html; charset=utf-8',
|
||||
@@ -243,8 +358,8 @@ export function createPortalPublishedPageDelivery({
|
||||
res.set(
|
||||
'Content-Security-Policy',
|
||||
wechatShare
|
||||
? "default-src 'none'; style-src 'unsafe-inline'; img-src data: https:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' https://cdn.jsdelivr.net https://res.wx.qq.com; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"
|
||||
: "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' https://cdn.jsdelivr.net; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'",
|
||||
? "default-src 'none'; style-src 'unsafe-inline'; img-src data: https:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' 'self' https://cdn.jsdelivr.net https://res.wx.qq.com; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"
|
||||
: "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' 'self' https://cdn.jsdelivr.net; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'",
|
||||
);
|
||||
res.set(
|
||||
'Cache-Control',
|
||||
|
||||
@@ -61,6 +61,35 @@ function createResult(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
test('injects rybbit tracker for publication HTML delivery', async () => {
|
||||
const sendPublishedPage =
|
||||
createPortalPublishedPageDelivery({
|
||||
async registerLongImageArtifact() {},
|
||||
rybbitConfig: {
|
||||
enabled: true,
|
||||
siteId: '2',
|
||||
idSecret: 'secret',
|
||||
scriptPath: '/rybbit/script.js',
|
||||
hostPath: '/rybbit',
|
||||
},
|
||||
});
|
||||
const response = createResponse();
|
||||
await sendPublishedPage(
|
||||
createRequest(),
|
||||
response,
|
||||
{
|
||||
...createResult(),
|
||||
ownerId: 'user-123',
|
||||
pageSource: { pageId: 'page-1' },
|
||||
publication: { id: 'pub-1', accessMode: 'login_required' },
|
||||
},
|
||||
{ raw: true },
|
||||
);
|
||||
assert.match(response.body, /src="\/rybbit\/script\.js"/);
|
||||
assert.match(response.body, /data-site-id="2"/);
|
||||
assert.doesNotMatch(response.body, /user-123/);
|
||||
});
|
||||
|
||||
test('wraps a normal full HTML publication in the share shell', async () => {
|
||||
const sendPublishedPage =
|
||||
createPortalPublishedPageDelivery({
|
||||
|
||||
Reference in New Issue
Block a user