feat: add attributable page analytics

This commit is contained in:
john
2026-07-20 20:49:44 +08:00
parent d479e89fc7
commit 1c82b34ad9
12 changed files with 144 additions and 11 deletions
@@ -1,6 +1,7 @@
import {
resolveAnalyticsOwnerLabel,
resolveAnalyticsOwnerSegment,
resolveAnalyticsPlan,
sendMindSpaceAnalyticsEvent,
} from '../mindspace-analytics.mjs';
import {
@@ -43,6 +44,7 @@ export async function bootstrapPortalIntegrationServices({
resolveAnalyticsOwnerSegment,
resolveAnalyticsOwnerLabelFn =
resolveAnalyticsOwnerLabel,
resolveAnalyticsPlanFn = resolveAnalyticsPlan,
sendMindSpaceAnalyticsEventFn =
sendMindSpaceAnalyticsEvent,
sendMindSpaceRybbitEventFn =
@@ -155,6 +157,8 @@ export async function bootstrapPortalIntegrationServices({
ownerLabel: resolveAnalyticsOwnerLabelFn(
pageOwner,
),
planType: resolveAnalyticsPlanFn(pageOwner),
generatedAt: new Date().toISOString(),
pageId: artifact.relativePath,
publicationId: sessionId,
agentRunId: sessionId,
@@ -148,6 +148,10 @@ function createSetup(overrides = {}) {
calls.push(['owner-label', user]);
return 'label';
},
resolveAnalyticsPlanFn(user) {
calls.push(['plan-type', user]);
return 'pro';
},
sendMindSpaceAnalyticsEventFn(event) {
calls.push(['analytics', event]);
return Promise.resolve();
@@ -349,12 +353,18 @@ test('preserves generated-page analytics projection', async () => {
ownerId: 'user-1',
ownerSegment: 'segment',
ownerLabel: 'label',
planType: 'pro',
generatedAt: analyticsCall[1].generatedAt,
pageId: 'public/page.html',
publicationId: 'session-1',
agentRunId: 'session-1',
channel: 'wechat_mp',
url: 'https://example/page',
});
assert.match(
analyticsCall[1].generatedAt,
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
);
const rybbitCall = setup.calls.find(
([name]) => name === 'rybbit',
);
+4
View File
@@ -9,6 +9,7 @@ import {
import {
resolveAnalyticsOwnerLabel,
resolveAnalyticsOwnerSegment,
resolveAnalyticsPlan,
sendMindSpaceAnalyticsEvent,
} from '../mindspace-analytics.mjs';
import {
@@ -96,6 +97,7 @@ export function attachPortalSessionRoutes(
resolveAnalyticsOwnerSegment,
resolveAnalyticsOwnerLabelFn =
resolveAnalyticsOwnerLabel,
resolveAnalyticsPlanFn = resolveAnalyticsPlan,
syncPublicHtmlAfterFinishFn = syncPublicHtmlAfterFinish,
resolveMindSpaceRuntimeConfigFn =
resolveMindSpaceRuntimeConfig,
@@ -432,6 +434,8 @@ export function attachPortalSessionRoutes(
resolveAnalyticsOwnerSegmentFn(req.currentUser),
ownerLabel:
resolveAnalyticsOwnerLabelFn(req.currentUser),
planType: resolveAnalyticsPlanFn(req.currentUser),
generatedAt: new Date().toISOString(),
pageId: relativePath,
publicationId: sessionId,
agentRunId: sessionId,
+6
View File
@@ -165,6 +165,7 @@ function createDependencies(overrides = {}) {
sendMindSpaceAnalyticsEventFn: async () => {},
resolveAnalyticsOwnerSegmentFn: () => 'segment',
resolveAnalyticsOwnerLabelFn: () => 'label',
resolveAnalyticsPlanFn: () => 'pro',
syncPublicHtmlAfterFinishFn: async () => ({
publicHtmlRelativePaths: [],
}),
@@ -524,6 +525,11 @@ test('stream hook uses current session id for contracts and analytics', async ()
assert.equal(calls[1].kind, 'materialize');
assert.equal(calls[2].input.publicationId, 'session-1');
assert.equal(calls[2].input.agentRunId, 'session-1');
assert.equal(calls[2].input.planType, 'pro');
assert.match(
calls[2].input.generatedAt,
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
);
});
test('Finish hook preserves refresh, sync, delivery readiness, memory, and lock lifecycle', async () => {
@@ -7,6 +7,7 @@ import {
injectMindSpaceAnalytics,
resolveAnalyticsOwnerLabel,
resolveAnalyticsOwnerSegment,
resolveAnalyticsPlan,
} from '../mindspace-analytics.mjs';
import {
injectMindSpaceRybbit,
@@ -303,6 +304,8 @@ export function createPortalWorkspacePublicationDelivery({
ownerLabel: resolveAnalyticsOwnerLabel(
pageOwner ?? {},
),
planType: resolveAnalyticsPlan(pageOwner ?? {}),
generatedAt: pageDataContext?.generatedAt ?? '',
pageId:
pageDataContext?.pageId ?? '',
publicationId: