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:
@@ -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