Strapi 后台 NPS 调查机制详解:展示规则、本地持久化与数据提交流程(源码级)
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
本文以 Strapi 官方文档docs/docs/docs/01-core/admin/04-features/nps.md为骨架,结合管理端源码packages/core/admin/admin/src/components/NpsSurvey.tsx及其单元测试,完整拆解后台 NPS(Net Promoter Score,净推荐值)调查的功能定位、展示时机规则、评分交互、localStorage 持久化字段、useNpsSurveySettingsHook 在注册流程中的联动,以及反馈数据上报的端点与请求体结构。读完后你可以准确回答:NPS 问卷何时出现、为什么有时不出现、哪些数据被发送、以及如何在自建环境中通过 feature flag 关闭它。
一、NPS 调查是什么、做什么
官方文档 nps.md 对其定位一句话概括:NPS 调查用于收集用户(管理员)对 Strapi 的反馈,采用 0 到 10 的评分量表,并邀请用户填写附加评论。
从源码实现看,NpsSurvey.tsx 中ratingArray由[...Array(11).keys()]生成,即 0–10 共 11 个分值,与文档描述一致。问卷主体包含:
- 一条固定问题文案(i18n id:
app.components.NpsSurvey.banner-title):"How likely are you to recommend Strapi to a friend or colleague?"(你有多大可能向朋友或同事推荐 Strapi?),两端分别标注 "Not at all likely" 与 "Extremely likely"; - 桌面端渲染为 11 个可选数字方块(隐藏 radio 输入 + 可见数字标签,见 NpsSurvey.tsx);移动端则降级为
SingleSelect下拉选择(NpsSurvey.tsx); - 选中评分后才展开改进建议文本框(
Textarea)与 "Submit Feedback" 按钮(NpsSurvey.tsx); - 右上角始终提供 "Dismiss survey" 关闭按钮(NpsSurvey.tsx)。
整个表单由 Formik 驱动,yup校验规则要求npsSurveyRating必填(yup.number().required()),评分未选择时提交按钮区域不会渲染,从交互层面保证了文档所说的"评分量表"是强制项。
二、什么时候展示问卷:准入条件与冷却规则
文档给出的展示规则是:
- 只对注册时勾选了 "Keep me updated" 复选框的管理员用户展示;
- 用户在后台活动一段时间后才展示(文档表述为 "after 5 minutes of activity");
- 重提频率规则:
- 用户响应过调查后,90 天内不再重复展示;
- 首次未响应(关闭)后,7 天后再展示;
- 第二次及以后未响应后,90 天后再展示。
源码中这些规则全部集中在纯函数checkIfShouldShowSurvey(NpsSurvey.tsx)与冷却常量delays(NpsSurvey.tsx)中:
const delays = { postResponse: 90 * 24 * 60 * 60 * 1000, // 90 days in ms postFirstDismissal: 14 * 24 * 60 * 60 * 1000, // 14 days in ms postSubsequentDismissal: 90 * 24 * 60 * 60 * 1000, // 90 days in ms display: 30 * 60 * 1000, // 30 minutes in ms };注意:以当前仓库源码为准,首次关闭后的冷却实际为14 天(postFirstDismissal)、展示前的会话延迟为30 分钟(delays.display),与文档描述的 7 天/5 分钟存在出入,可推断文档记录的是早期版本的参数,源码才是当前行为的事实依据。
展示判定的五个分支
checkIfShouldShowSurvey按优先级依次判断(源码注释原文明确了这条决策链):
- 全局开关:
window.strapi.flags.nps === false时直接不展示(NpsSurvey.tsx); - 用户级开关:持久化设置中
enabled === false(即注册时未接受 "Keep me updated")时不展示(NpsSurvey.tsx); - 已响应过:存在
lastResponseDate时,距上次响应 ≥ 90 天才重新展示,否则不展示(NpsSurvey.tsx); - 关闭过两次及以上:存在
lastDismissalDate时,距上次关闭 ≥ 90 天才重新展示(NpsSurvey.tsx); - 只关闭过一次:存在
firstDismissalDate时,距首次关闭 ≥ 14 天才重新展示(NpsSurvey.tsx);
以上均不满足则展示。源码注释还特别说明了两点细节:提交一次响应会重置关闭计数(firstDismissalDate/lastDismissalDate被置空,见下文第四节),且分支 4、5 的判断顺序不能颠倒——关闭过两次的用户必然也有firstDismissalDate,必须先按"多次关闭"分支处理。
此外,组件首次渲染时调用checkIfShouldShowSurvey的结果存入surveyIsShown状态,且只在首次渲染时检查一次(NpsSurvey.tsx);随后用setTimeout(delays.display)设置一个"展示冷却",30 分钟计时结束前组件直接返回null(NpsSurvey.tsx)。这解释了文档中"活动一段时间后展示"的机制——从当前实现看,它是会话开始后的固定延迟而非逐字面的"5 分钟活动量"统计。
三、问卷状态的本地持久化:localStorage 结构与写入时机
所有展示状态都通过usePersistentState持久化到浏览器 localStorage,键名为STRAPI_NPS_SURVEY_SETTINGS。该 Hook 定义于 usePersistentState.ts:初始值从 localStorage 的 JSON 反序列化读取,随后在useLayoutEffect中同步写回——同步写入保证了同标签页跳转等场景下状态不丢失。NPS 专用的封装是useNpsSurveySettings(NpsSurvey.tsx):
function useNpsSurveySettings() { const [npsSurveySettings, setNpsSurveySettings] = usePersistentState<NpsSurveySettings>( 'STRAPI_NPS_SURVEY_SETTINGS', { enabled: true, lastResponseDate: null, firstDismissalDate: null, lastDismissalDate: null, } ); return { npsSurveySettings, setNpsSurveySettings }; }对应的NpsSurveySettings接口(NpsSurvey.tsx)四个字段含义如下:
| 字段 | 类型 | 含义 | 写入时机 |
|---|---|---|---|
enabled | boolean | 用户是否接受接收调查(注册时勾选 "Keep me updated") | 注册流程写入;默认值为true |
lastResponseDate | string \| null | 上次成功提交响应的时间 | 提交成功回调中写入 |
firstDismissalDate | string \| null | 第一次关闭问卷的时间 | 首次点击 "Dismiss survey" 时写入 |
lastDismissalDate | string \| null | 最近一次关闭问卷的时间 | 第二次及以后关闭时写入 |
两个写状态的路径(源码行为事实):
- 提交成功(
handleSubmitResponse,NpsSurvey.tsx):写入lastResponseDate,同时把firstDismissalDate与lastDismissalDate重置为null——即文档隐含的"提交响应重置关闭计数"; - 关闭问卷(
handleDismiss,NpsSurvey.tsx):清空lastResponseDate,若已有firstDismissalDate则写lastDismissalDate(进入 90 天冷却),否则写firstDismissalDate(进入 14 天冷却)。
单元测试 NpsSurvey.test.tsx 对上述行为逐条做了验证:'saves user response'、'saves first user dismissal'、'saves subsequent user dismissal'三个用例精确断言了 localStorage 中被写入的对象结构(如响应后的{ enabled: true, lastResponseDate, firstDismissalDate: null, lastDismissalDate: null })。
四、数据提交到哪:端点、请求体与错误处理
文档明确数据提交端点为https://analytics.strapi.io/submit-nps。源码中该地址支持环境变量覆盖(NpsSurvey.tsx):
const res = await fetch( `${process.env.STRAPI_ANALYTICS_URL || 'https://analytics.strapi.io'}/submit-nps`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), } );请求体结构由组件内的NpsSurveyMutationBody接口定义(NpsSurvey.tsx),各字段取值来源如下:
| 字段 | 类型 | 取值来源 |
|---|---|---|
email | string | 当前登录管理员邮箱(useAuth的user.email),非对象用户取空串 |
rating | number \| null | 表单选中的 0–10 评分 |
comment | string | 改进建议文本框内容 |
environment | string? | useAppInfo读取的currentEnvironment |
version | string? | useAppInfo读取的strapiVersion |
license | 'Enterprise' \| 'Growth' \| 'Community' | 全局window.strapi.projectType |
isHostedOnStrapiCloud | boolean | process.env.STRAPI_HOSTING === 'strapi.cloud' |
提交后的状态流转:成功(res.ok)则更新本地持久化设置、显示 "Thank you very much for your feedback!" 感谢语,并在 3 秒后收起问卷(NpsSurvey.tsx);失败则弹出 danger 通知 "An error occurred" 且问卷保持打开,允许用户重试(NpsSurvey.tsx)。测试用例'show error message if request fails and keep survey open'(NpsSurvey.test.tsx)用 MSW 拦截该端点返回 500,验证了错误通知出现且感谢语不出现。
五、useNpsSurveySettings 在注册流程中的联动
文档 Hooks 小节指出:useNpsSurveySettings基于usePersistentState实现,之所以导出该 Hook,是为了在注册过程中判断用户是否勾选了 "Keep me updated" 复选框,从而决定是否启用调查。
这条链路在源码中的实际形态(实现事实):注册页 Register.tsx 渲染复选框,文案为 "Keep me updated about new features & upcoming improvements (by doing this you accept the {terms} and the {policy}.)"。创建管理员用户成功后,若news为真,则调用setNpsSurveySettings((s) => ({ ...s, enabled: true }))(Register.tsx),并跳转/usecase收集使用场景;未勾选则仅跳回首页,enabled保持默认状态、问卷永远不会对该用户展示。创建普通用户的handleRegisterUser分支有完全相同的逻辑(Register.tsx)。
需要说明一个语义细节:enabled字段默认值本身就是true,注册流程做的是显式确认写true;源码注释写的是 "Only enable EE survey if user accepted the newsletter"。因此对存量用户(在 NPS 功能上线前创建账号、没有机会勾选该复选框的人),checkIfShouldShowSurvey的注释明确说明"假设他们若有机会会选择开启",即默认按开启处理。
六、自建部署中的控制手段:window.strapi.flags.nps
checkIfShouldShowSurvey的第一个分支检查window.strapi.flags.nps。这个全局对象在 render.ts 中初始化:
projectType: 'Community', flags: { nps: false, promoteEE: true, docLinks: true, },可以看到flags.nps的默认值是false,随后会在渲染初始化流程中依据项目/许可上下文覆写。因此从源码结构看,自建(非托管)实例默认不会收到 NPS 问卷,问卷主要出现在 Strapi 官方渠道创建的项目中——这一推断与文档"只对勾选了 Keep me updated 的管理员展示"的准入前提互为印证。对本地开发者而言,实际可控的开关是:
- 注册时不勾选 "Keep me updated":
enabled不会为真,问卷不出现; - 构建期环境变量
STRAPI_ANALYTICS_URL:可替换上报域名; - 全局 flag
window.strapi.flags.nps:服务端渲染注入的标志位,置false后展示判定直接短路返回。
七、测试矩阵:展示规则如何被逐条验证
NpsSurvey.test.tsx 以 localStorage mock(键STRAPI_NPS_SURVEY_SETTINGS,见 测试文件)+ Jest 假定时器构建了完整的规则验证矩阵,与上文展示判定一一对应:
'renders survey if enabled'/'renders survey if settings don't exist':enabled: true或设置不存在时问卷渲染;'does not render survey if disabled':enabled: false时不渲染;'respects the delay after user submission':以 2020-01-01 为响应时间,30 天内不展示,90 天(2020-03-31)后重新展示(测试文件);'respects the delay after first user dismissal':首次关闭后 7 天内不展示、14 天后(2020-01-15)重新展示——该用例的时间点直接印证了源码中 14 天(而非文档所述 7 天)的冷却值(测试文件);'respects the delay after subsequent user dismissal':第二次关闭后 90 天才重新展示(测试文件)。
八、小结:NPS 调查的完整生命周期
把文档与源码拼合起来,一次 NPS 调查的完整生命周期是:
- 注册:管理员勾选 "Keep me updated",
useNpsSurveySettings将enabled: true写入 localStorage(Register.tsx); - 准入判定:
checkIfShouldShowSurvey依次检查全局 flag、enabled、响应/关闭冷却(NpsSurvey.tsx); - 延迟展示:通过判定后,再等待会话内的展示冷却(当前源码为 30 分钟)后在页面底部渲染固定问卷(NpsSurvey.tsx);
- 交互:用户选 0–10 评分 + 可选评论后提交,或点击关闭;两条路径分别更新
lastResponseDate或first/lastDismissalDate(NpsSurvey.tsx); - 上报:成功时 POST 邮箱、评分、评论、环境、版本、许可类型等到 analytics 端点,随后展示感谢语并在 90/14/90 天冷却节奏中等待下一次触达。
对维护 Strapi 后台或在其上开发自定义插件的工程师来说,这套实现的价值不仅在于功能本身:usePersistentState键名约定(STRAPI_NPS_SURVEY_SETTINGS)、"注册即授权"的数据合规做法,以及"纯函数判定 + 假定时器测试"的规则验证方式,都是可直接借鉴到自建管理端功能(如使用引导、版本升级提示)中的成熟模式。
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考