fix: inject Rybbit tracking on m.tkmind.cn publication pages
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:
john
2026-07-27 22:10:07 +08:00
parent 48c61a3279
commit c7718c683b
9 changed files with 199 additions and 8 deletions
@@ -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 () => {
+1 -1
View File
@@ -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; }
+117 -2
View File
@@ -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({