news 2026/9/7 18:00:29

Strapi 后台 NPS 调查机制详解:展示规则、本地持久化与数据提交流程(源码级)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Strapi 后台 NPS 调查机制详解:展示规则、本地持久化与数据提交流程(源码级)

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()),评分未选择时提交按钮区域不会渲染,从交互层面保证了文档所说的"评分量表"是强制项。

二、什么时候展示问卷:准入条件与冷却规则

文档给出的展示规则是:

  1. 只对注册时勾选了 "Keep me updated" 复选框的管理员用户展示;
  2. 用户在后台活动一段时间后才展示(文档表述为 "after 5 minutes of activity");
  3. 重提频率规则:
    • 用户响应过调查后,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按优先级依次判断(源码注释原文明确了这条决策链):

  1. 全局开关window.strapi.flags.nps === false时直接不展示(NpsSurvey.tsx);
  2. 用户级开关:持久化设置中enabled === false(即注册时未接受 "Keep me updated")时不展示(NpsSurvey.tsx);
  3. 已响应过:存在lastResponseDate时,距上次响应 ≥ 90 天才重新展示,否则不展示(NpsSurvey.tsx);
  4. 关闭过两次及以上:存在lastDismissalDate时,距上次关闭 ≥ 90 天才重新展示(NpsSurvey.tsx);
  5. 只关闭过一次:存在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)四个字段含义如下:

字段类型含义写入时机
enabledboolean用户是否接受接收调查(注册时勾选 "Keep me updated")注册流程写入;默认值为true
lastResponseDatestring \| null上次成功提交响应的时间提交成功回调中写入
firstDismissalDatestring \| null第一次关闭问卷的时间首次点击 "Dismiss survey" 时写入
lastDismissalDatestring \| null最近一次关闭问卷的时间第二次及以后关闭时写入

两个写状态的路径(源码行为事实):

  • 提交成功handleSubmitResponse,NpsSurvey.tsx):写入lastResponseDate,同时把firstDismissalDatelastDismissalDate重置为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),各字段取值来源如下:

字段类型取值来源
emailstring当前登录管理员邮箱(useAuthuser.email),非对象用户取空串
ratingnumber \| null表单选中的 0–10 评分
commentstring改进建议文本框内容
environmentstring?useAppInfo读取的currentEnvironment
versionstring?useAppInfo读取的strapiVersion
license'Enterprise' \| 'Growth' \| 'Community'全局window.strapi.projectType
isHostedOnStrapiCloudbooleanprocess.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:可替换上报域名;
  • 全局 flagwindow.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 调查的完整生命周期是:

  1. 注册:管理员勾选 "Keep me updated",useNpsSurveySettingsenabled: true写入 localStorage(Register.tsx);
  2. 准入判定checkIfShouldShowSurvey依次检查全局 flag、enabled、响应/关闭冷却(NpsSurvey.tsx);
  3. 延迟展示:通过判定后,再等待会话内的展示冷却(当前源码为 30 分钟)后在页面底部渲染固定问卷(NpsSurvey.tsx);
  4. 交互:用户选 0–10 评分 + 可选评论后提交,或点击关闭;两条路径分别更新lastResponseDatefirst/lastDismissalDate(NpsSurvey.tsx);
  5. 上报:成功时 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 17:58:50

AI汽车软件开发:从代码生成到能力边界落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 17:58:36

SpringBoot猫咖管理系统开发与优化实践

1. 项目概述&#xff1a;猫咖管理系统的核心价值养猫人群的快速增长催生了猫咖这一新兴业态。作为结合猫咪互动与咖啡休闲的复合空间&#xff0c;猫咖日常运营涉及会员管理、猫咪健康记录、预约服务等多维度事务。传统手工记录方式效率低下且易出错&#xff0c;这正是我们开发&…

作者头像 李华
网站建设 2026/9/7 17:58:12

AgentScope 2.0实战:多智能体编排、工具调用与流式输出全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 17:56:34

DETR目标检测:Transformer如何用集合预测取代NMS与anchor

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华