diff --git a/.cursor/rules/mindspace-analytics-creator-username.mdc b/.cursor/rules/mindspace-analytics-creator-username.mdc new file mode 100644 index 0000000..4e15bc9 --- /dev/null +++ b/.cursor/rules/mindspace-analytics-creator-username.mdc @@ -0,0 +1,26 @@ +--- +description: MindSpace 生成页埋点必须上报创建者 username,且不得用 owner_id identify 匿名访客 +globs: mindspace-analytics.mjs,mindspace-rybbit.mjs,mindspace-analytics.test.mjs,mindspace-rybbit.test.mjs,server/portal-session-routes.mjs,server/portal-integration-services-bootstrap.mjs +alwaysApply: false +--- + +# MindSpace Analytics — 创建用户昵称与 Public 身份 + +Umami「创建用户」依赖 `event_data.username` / session `username`。路径里的 MindSpace UUID **不是**昵称。 + +## 必须 + +- 服务端 `page_generated`(`sendMindSpaceAnalyticsEvent` / `sendMindSpaceRybbitEvent`)的事件属性 **必须**包含 `username`(创建者展示名,来自 `resolveAnalyticsOwnerLabel`) +- 可同时保留 `owner_label` 作兼容,值与 `username` 相同 +- Public HTML 内嵌脚本的 `metadata` **必须**继续带 `username`(给 scroll/click 等客户端事件) + +## 禁止(避免破坏访客统计) + +- Public **匿名**访问路径 **禁止** `umami.identify(owner_id, …)` / 把创建者 ID 当作访客 `id` +- 仅已登录 viewer 可 `identifyViewer()`;不得为了补创建用户昵称而恢复「全员 identify 创建者」 + +## 改完必跑 + +```bash +node --test mindspace-analytics.test.mjs mindspace-rybbit.test.mjs +``` diff --git a/mindspace-analytics.mjs b/mindspace-analytics.mjs index 969949d..0ce9f23 100644 --- a/mindspace-analytics.mjs +++ b/mindspace-analytics.mjs @@ -123,6 +123,9 @@ export function sendMindSpaceAnalyticsEvent({ const owner = resolveAnalyticsIdentity(ownerId, config); if (!owner) return Promise.resolve(false); const endpoint = `${String(config.analyticsUrl || 'http://127.0.0.1:3100').replace(/\/$/, '')}/api/send`; + // Umami Memind dashboards resolve「创建用户」from event_data.username (not owner_label). + // Keep owner_label for backward compatibility; never omit username on generation events. + const username = resolveAnalyticsOwnerLabel({ displayName: ownerLabel }); const payload = { website: config.websiteId, id: owner, @@ -137,7 +140,8 @@ export function sendMindSpaceAnalyticsEvent({ channel, owner_segment: String(ownerSegment || 'unknown'), plan_type: String(planType || 'unknown'), - owner_label: resolveAnalyticsOwnerLabel({ displayName: ownerLabel }), + username, + owner_label: username, generated_at: String(generatedAt || ''), identity_mode: config.identityMode === 'raw' ? 'raw' : 'pseudonymous', }, diff --git a/mindspace-analytics.test.mjs b/mindspace-analytics.test.mjs index 30f49c3..b2d89e6 100644 --- a/mindspace-analytics.test.mjs +++ b/mindspace-analytics.test.mjs @@ -280,6 +280,7 @@ test('generation events are attributed to raw identity and include analysis dime assert.equal(requestBody.payload.id, 'user-123'); assert.deepEqual(requestBody.payload.data, expectPayload({ owner_id: 'user-123', + username: '张三', owner_label: '张三', owner_segment: 'plan:pro', plan_type: 'pro', diff --git a/mindspace-rybbit.mjs b/mindspace-rybbit.mjs index 95f3cc2..a58c14c 100644 --- a/mindspace-rybbit.mjs +++ b/mindspace-rybbit.mjs @@ -60,6 +60,7 @@ export function sendMindSpaceRybbitEvent({ const owner = pseudonymizeAnalyticsId(ownerId, config.idSecret); if (!owner) return Promise.resolve(false); const endpoint = `${String(config.rybbitUrl || 'https://rybbit.tkmind.cn').replace(/\/$/, '')}/api/track`; + const username = resolveAnalyticsOwnerLabel({ displayName: ownerLabel }); const payload = { site_id: String(config.siteId), type: 'custom_event', @@ -76,7 +77,8 @@ export function sendMindSpaceRybbitEvent({ agent_run_id: String(agentRunId || ''), channel, owner_segment: String(ownerSegment || 'unknown'), - owner_label: resolveAnalyticsOwnerLabel({ displayName: ownerLabel }), + username, + owner_label: username, }), }; return fetch(endpoint, {