news 2026/9/16 17:20:03

Corsair Formbricks 插件接入指南:47 个操作、双版本 API 与风险分级的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Corsair Formbricks 插件接入指南:47 个操作、双版本 API 与风险分级的完整实践

Corsair Formbricks 插件接入指南:47 个操作、双版本 API 与风险分级的完整实践

【免费下载链接】corsairConnect your users to their apps项目地址: https://gitcode.com/GitHub_Trending/corsa/corsair

本篇技术指南以 packages/formbricks/README.md 为核心,结合该包源码(packages/formbricks/index.tsclient.tsendpoints/schema/)系统讲解如何在 Corsair 中集成 Formbricks——一个开源的用户体验管理平台,用于应用内问卷与用户反馈收集。读完本文,你将掌握插件的安装与认证方式、47 个端点操作与风险等级的完整映射、Formbricks 同时暴露 v1/v2 两套 API 时的路由选择策略,以及错误重试、数据镜像与实时测试的最佳实践。

插件定位与安装

@corsair-dev/formbricks是 Corsair 生态中专门连接 Formbricks 的官方插件,包描述为 "Formbricks plugin for Corsair"。它把 Formbricks Cloud(以及自托管实例)的 API 能力封装为结构化的端点注册表,供 Corsair 的权限、审计、错误处理与数据镜像体系统一调度。

安装非常简单,通过 pnpm 添加即可:

pnpm add @corsair-dev/formbricks

从 packages/formbricks/package.json 可以看到,该包以 ESM 形式发布("type": "module"),运行时入口为./dist/index.js,类型声明为./dist/index.d.ts。它声明了如下 peer 依赖,使用时需保证宿主环境已安装:

  • corsair>=0.1.0,Corsair 核心运行时;
  • zod^4.1.13,所有端点输入/输出 schema 的校验库。

源码包内提供了构建、类型检查与测试脚本(见 package.json):

pnpm build # 清空 dist 后由 tsc 与 tsup 完成构建 pnpm typecheck # 仅做类型检查 pnpm test # 运行 jest 单元测试(client.test.ts、endpoints.test.ts、schema.test.ts) pnpm test:live # 运行需要真实凭据的集成测试(详见下文"实时集成测试")

认证:API Key 与作用域陷阱

Formbricks 的认证方式为API key:请求时在x-api-key请求头中携带个人 API key(在 Formbricks 控制台 Settings → API Keys 中创建),而不是 Bearer Token。两份 OpenAPI 文档都只声明了apiKeyAuth这一种安全方案,目录中没有 OAuth 表面,因此插件只声明了api_key一种认证类型(见 index.ts 的formbricksAuthConfig)。

首次使用时,Corsair 会提示你的租户提供凭据;插件通过keyBuilder(index.ts)在端到端调用时解析该 key:若插件选项里显式传了key则直接使用,否则从ctx.keys.get_api_key()读取,取不到即抛出AuthMissingError

比大多数服务商更关键的一点是 key 的作用域。Formbricks 的 API key 可以是组织级(organization-scoped)或工作区级(workspace-scoped):

  • 组织级 key 在几乎所有的工作区级管理路由上都会返回 401——而这类路由恰恰占大多数;
  • me.get操作可以报告当前 key 属于哪种作用域;
  • 插件的AUTH_ERROR处理器也会在日志中提示这一点,因为作用域不匹配的失败看起来很像"key 无效",实际却是"key 作用域不对"。

插件还支持host选项来覆盖 API 主机,用于对接自托管 Formbricks 实例(见 index.ts 的FormbricksPluginOptions)。默认主机是 Formbricks 云https://app.formbricks.com(见 client.ts 的FORMBRICKS_CLOUD_HOST)。需要说明的是:插件默认针对云端验证通过;自托管在 Formbricks 上游虽受支持,但需要 Ubuntu 虚拟机、自定义域名与 80/443 端口,本仓库未对其做过实测,host选项存在只是因为两种部署的差异仅在于主机地址。

端点全景:47 个操作、46 个目录 id、38 条路由

插件的端点注册表(index.ts 的formbricksEndpointsNested)覆盖了目录中的全部 46 个 id,共实现47 个操作、38 条不同路由。三个数字不一致并非疏漏,而是如实反映了几类值得注意的情况:4 个操作是别名、3 个已移除路由的 id 对应真实能力、1 个操作没有目录 id、还有多个操作共享同一条 URL。下表完整继承自 README.md,并按风险等级组织:

读操作(read)

操作操作 ID说明
actionClasses.listformbricks.api.actionClasses.list列出可触发问卷的 action class
client.environmentformbricks.api.client.environment读取工作区的客户端环境包
contactAttributeKeys.getformbricks.api.contactAttributeKeys.get获取单个联系属性键
contactAttributeKeys.getClassformbricks.api.contactAttributeKeys.getClass以目录中旧的 "attribute class" 名称获取属性键——与get同路由
contactAttributeKeys.listformbricks.api.contactAttributeKeys.list列出工作区定义的联系属性键
contactAttributeKeys.listClassesformbricks.api.contactAttributeKeys.listClasses以旧的 "attribute class" 名称列出属性键——与list同路由
contactAttributes.listformbricks.api.contactAttributes.list跨联系人列出属性值
contacts.getformbricks.api.contacts.get获取单个联系人
contacts.getPersonformbricks.api.contacts.getPerson以旧的 "person" 名称获取联系人——与get同路由
contacts.listformbricks.api.contacts.list列出工作区内的联系人
contacts.listPeopleformbricks.api.contacts.listPeople以旧的 "people" 名称列出联系人——与list同路由
health.checkformbricks.api.health.check检查服务健康状态
health.listformbricks.api.health.list读取服务健康状态
me.getformbricks.api.me.get获取 API key 的身份、工作区与组织
me.getAccountInfoformbricks.api.me.getAccountInfo获取账户信息
me.getManagementformbricks.api.me.getManagement仅限工作区级 key,获取 v1 账户负载
responses.listformbricks.api.responses.list列出问卷回复,可选按单个问卷过滤
roles.listformbricks.api.roles.list列出成员可持有的组织角色
surveys.listformbricks.api.surveys.list列出工作区内的问卷
teams.listformbricks.api.teams.list列出组织内的团队
teams.listWorkspaceTeamsformbricks.api.teams.listWorkspaceTeams列出哪些团队可访问哪些工作区
webhooks.getformbricks.api.webhooks.get获取单个 webhook
webhooks.listformbricks.api.webhooks.list列出工作区上的 webhook

写操作(write)

操作操作 ID说明
actionClasses.createformbricks.api.actionClasses.create创建 action class
client.contactsStateformbricks.api.client.contactsState读取受访者状态,userId 为新的则创建联系人
client.createDisplayformbricks.api.client.createDisplay记录问卷曾展示给某人
client.createUserformbricks.api.client.createUser创建客户端用户
client.identifyUserformbricks.api.client.identifyUser创建或识别客户端用户
contactAttributeKeys.createformbricks.api.contactAttributeKeys.create创建联系属性键
contactAttributeKeys.updateformbricks.api.contactAttributeKeys.update更新联系属性键的定义——不涉及任何联系人取值
contacts.createformbricks.api.contacts.create创建联系人
contacts.updateAttributesformbricks.api.contacts.updateAttributes按 userId 设置联系人属性值,联系人不存在则创建
contacts.uploadBulkformbricks.api.contacts.uploadBulk一次请求创建大量联系人
responses.createformbricks.api.responses.create记录一次问卷回复
responses.updateformbricks.api.responses.update更新一次问卷回复
storage.uploadPrivateformbricks.api.storage.uploadPrivate为私有受访者文件申请上传
storage.uploadPublicformbricks.api.storage.uploadPublic为公共文件申请上传
surveys.createformbricks.api.surveys.create创建问卷
surveys.updateformbricks.api.surveys.update更新问卷
webhooks.createformbricks.api.webhooks.create创建 webhook 并接收其签名密钥
webhooks.updateformbricks.api.webhooks.update更新 webhook

破坏性操作(destructive)

操作操作 ID说明
contactAttributeKeys.deleteformbricks.api.contactAttributeKeys.delete删除联系属性键
contacts.deleteformbricks.api.contacts.delete删除联系人,移除受访者身份
responses.deleteformbricks.api.responses.delete删除问卷回复,抹除受访者提交的答案
surveys.deleteformbricks.api.surveys.delete删除问卷及其收集到的回复
teams.deleteformbricks.api.teams.delete删除团队,撤销其授予的工作区访问权
webhooks.deleteformbricks.api.webhooks.delete删除 webhook,使其签名密钥失效

风险等级为什么这样划分

风险等级按"操作能毁掉什么"而非"用了哪个 HTTP 方法"来划分,index.ts 的formbricksEndpointMeta中给出了若干不太直观的决策依据:

  • surveys.delete是 destructive 而非 write:它会连同问卷收集到的回复一起删除——那是受访者数据,不只是配置;
  • webhooks.delete是 destructive:重建订阅会签发新的签名密钥,所有校验签名的接收端都得跟着更新,爆炸半径远大于一条记录;
  • teams.delete是 destructive:团队可能是成员访问工作区的唯一通道,删除会导致他人连带失去访问权;
  • responses.delete是 destructive,同时也是工作区所有者响应擦除请求(erasure request)时会用的操作,因此必须如实上报结果,不能乐观处理;
  • contacts.delete是 destructive:它移除一个人的身份并解除其回复的关联;
  • contacts.uploadBulk是 write 而非 read,而且是这里单次量最大的操作:一次创建大量联系人,重放会重复整个批次。

别名与共享路由背后的设计考量

四个别名操作(contacts.listPeoplecontacts.getPersoncontactAttributeKeys.listClassescontactAttributeKeys.getClass)是因为 Formbricks 把 "people" 改名为 "contacts"、"attribute classes" 改名为 "contact attribute keys" 并删除了旧路由,但目录仍同时保留两个名字。它们调用的是完全相同的 URL,存在的意义是让按旧条目工作的调用方不会 404——而不是背后有第二套能力。endpoints.test.ts会断言每个别名仍指向其主操作的路由,且每个别名发出自己的审计事件,使两者在日志中可区分(见 contacts.ts)。

另有三个已移除路由的 id 是真实能力,映射到当前路由实现:DELETE_PERSONcontacts.deleteCREATE_ATTRIBUTE_CLASScontactAttributeKeys.createDELETE_ATTRIBUTE_CLASScontactAttributeKeys.delete。而UPDATE_CONTACT_ATTRIBUTES在管理面完全没有对应路由(五个候选路由要么 404 要么 405),因此contacts.updateAttributes改走客户端用户路由,按userIdupsert(详见下文"联系人"一节)。

contactAttributeKeys.update则是一个不认领任何目录 id的操作:它编辑属性键的定义(显示名与描述),目录里没有它的 id,但能力真实存在,故予以保留。

别名的存在并非唯一的路由共享原因:两个 health id、两个 v1meid、三个 post 到客户端用户路由的操作同样共享 URL——这正是 47 个操作落到 38 条路由的由来。

请求层的两个关键决策

client.ts 的makeFormbricksRequest统一负责发请求,其中两处决策与代码生成模板不同,且都直接影响正确性:

  1. 错误不包装。模板会捕获一切异常并重抛为FormbricksAPIError,这会丢弃ApiError及其携带的 HTTP 状态码。而本插件所有错误处理器都按状态码分类——删除流程更依赖区分 404 与 500 来判断记录是否已不存在。包装会摧毁这一切,因此ApiError被原样上抛。

  2. query 参数在所有方法上发送,而不只 GET。模板在写请求上会丢弃 query。Formbricks 在非 GET 请求上接受 query 参数,一个被静默丢弃的参数是最糟的 bug:请求成功执行了,但干的事和调用方要求的不是一回事。

Formbricks 的所有响应都包裹在{ data }信封中(v2 列表还会加meta),shared.ts 的unwrap在端点层统一解包,因此输出 schema 描述的是调用方实际拿到的记录,而非外层包装;meta被有意丢弃(理由见下文分页一节)。请求体则经compactBody剔除值为undefined的字段——Formbricks 区分"字段缺失"(保留原值)与"显式null"(清空值),序列化undefined两者都不是。

双版本 API:v1 与 v2 并存是常态而非迁移过渡

Formbricks 同时暴露v1 与 v2 两套 API,且操作面横跨两者(client.ts):

  • v1拥有问卷、回复、action class、联系人、联系属性与存储;
  • v2拥有 webhook、组织、团队、角色、健康检查、联系人批量上传与联系属性键的写操作。

这不是一场可以等它结束的迁移——两套版本承载着不同的资源,完整的插件必须同时使用两者。少数资源在两版中都存在,最典型的是 webhook:通过POST v1/webhooks创建的 webhook 会被GET v2/management/webhooks返回,说明这是同一存储的两个表面而非两种资源。遇到这种情况,插件选择 OpenAPI 文档描述的那个版本并在操作注释中说明。

因此每个调用都显式指定版本,而非设定默认值——默认值会把 v2-only 的请求悄悄发往 v1,得到一个看起来像"记录不存在"的 404。

版本差异在端点实现中处处可见,例如:

  • 问卷:四个操作全是 v1,v2 没有问卷 CRUD(surveys.ts);
  • 联系人:读走 v1、写走 v2——不是偏好,而是 v1 对联系人只暴露 GET,对属性键没有 create/delete(contacts.ts);
  • 联系属性键列表:读走v2,因为只有 v2 支持分页——v1 路由完全忽略limit返回全部键,v2 路由尊重limit+skip并返回meta: {total, ...},两者返回完全相同的十个字段(已逐字段比对验证);
  • 属性键更新:走v1,因为只有 v1 接受部分更新——v2 会重新校验整个请求体(详见下节)。

端点纵深解析:核心操作与输入输出契约

问卷(surveys)

问卷是整个 API 的锚点:回复、展示记录与 webhook 都引用问卷 id。四个操作均为 v1,且只镜像问卷实体——问卷的内容(问题、结束页、样式)不建模,因为其形状取决于每种问题类型,在 Formbricks 编辑器里创作,一个封闭 schema 会在用户用到插件未枚举的问题类型时拒绝合法问卷(见 schema/database.ts)。

  • surveys.createworkspaceId必须在请求体中,缺失返回 400"workspaceId must be provided"——尽管 API key 已经作用域到该工作区;返回200 而非 201(同一 API 的联系人创建却返回 201,调用方不能想当然)。questions由产品而非 schema 要求——空问题问卷 API 会接受但无法作答,故留为可选,以便创建壳问卷(Formbricks 编辑器自己就是这么做的);
  • surveys.updatePUT 而非 POST——v1 文档声称更新用 POST,但POST到条目路由返回405 且响应体为空(types.ts 与 surveys.ts 均验证过)。对同一 body 应用两次状态不变,故不在非幂等集合中;
  • surveys.delete:不可逆,且会带走回复,因此标记为 destructive;返回200 并带删除的记录,而非 204。镜像行做 best-effort 驱逐:问卷本身不含个人数据,陈旧行只是不整洁而非泄露——与 webhook 的强制驱逐形成对照。

回复(responses)

responses.list有一个重要契约:surveyId是该路由唯一真正生效的过滤器。目录还文档化了contactIdstartDateendDatefilterDateFieldsortByorder,但逐一用效果验证后确认它们全被接受(200)后忽略?contactId=<不存在的id>返回全部 3 行、不可能的日期范围也返回全部行、order=ascorder=desc顺序一致。因此这些参数不声明——一个静默返回所有受访者答案的过滤器比缺失更糟:调用方要一个人的回复却拿到所有人的,且无从察觉。

responses.update是全插件唯一为绕开服务端 bug 而设的必填输入:PUT v1/management/responses/{id}data缺失时返回500internal_server_error{finished: true}会崩,{data: {}, finished: true}则成功),本应返回 422。在 schema 里强制data必填,就把 500 变成了本地校验错误。有趣的是该操作不要求workspaceId(已验证不带它返回 200)。

联系人(contacts)与属性键(contact attribute keys)

这一族内部就划分了"镜像"与"不镜像"的边界(contacts.ts):

  • 属性键被镜像:它们是schemaemailuserIdfirstName……)而非取值,属于配置,量小,且是让属性行可读的查找表;
  • 联系人与属性值不镜像:它们是受访者身份与个人详情。属性行把attributeKeyIdvalue配对,镜像它等于把某人的邮箱地址存进本地。

几个值得注意的输入契约(全部来自 types.ts):

  • contacts.createattributes普通对象(按属性键为 key),返回201
  • contacts.uploadBulkattributes数组{attributeKey: {key, name}, value}——与单个创建形状不对称,把对象形式发过去是 422。本地强制两条约束(均源自 API 自身的报错):每批最多 250 行422 "Maximum 250 contacts allowed at a time."),且每行必须带email属性422 "Email attribute is required for contact at index 0",本地校验也按行报索引)。整批在实践上是原子的,422 重试不会重复插入;输出是{status, message}而非上传的联系人数组;
  • contacts.updateAttributes:对应目录的UPDATE_CONTACT_ATTRIBUTES,是全插件唯一不走管理路由的操作——管理面没有任何路由能设置联系人属性值(五个候选路由逐一实测全部 404/405),因此 post 到客户端用户路由client/{workspaceId}/user,按userId(调用方自己的标识)upsert:id 不存在就创建联系人,没有"仅更新"形态,这正是它被标为write而非 update、并列入非幂等集合的原因。它返回受访者状态而非联系人记录,需要最新记录得再调get
  • contactAttributeKeys.createdescription必填——缺失返回 422,尽管它读起来像文档字段;
  • contactAttributeKeys.update:编辑的是属性键的定义(显示名、描述),不碰任何联系人的存量值(已通过读取联系人修改前后的取值验证)。它走v1是因为 v2 要求整包提交:PUT v2 {name}返回 422 缺descriptionPUT v2 {description}返回 422 缺name,只有 v1 允许只带一个字段。值得警惕的是PUT v1/management/contact-attribute-keys/{id}并不在已发布的 v1 文档中,是实况验证得到的——规范本身不完整,对DELETE v1/management/contacts/{id}同样如此。

webhook:订阅管理是端点,不是触发器

Formbricks 在收到回复时会发出出站 webhook,插件中的五个 webhook 操作管理这些订阅——但它们是端点,不是 Corsair 触发器。目录列出的触发器数量为零,Webhooks 一节写着 "No webhooks",因此没有注册任何 webhook 处理器,生成的 webhook 脚手架也被移除而非留作死代码(index.ts)。

创建 webhook 的输入契约值得细看(types.ts),每个约束都来自 API 的 422 报错而非文档猜测:

  • source枚举为"user" | "zapier" | "make" | "n8n"user是 API 创建时的取值;
  • triggers枚举为"responseFinished" | "responseCreated" | "responseUpdated"responseCreated在受访者开始时触发、responseUpdated在每次保存时触发、responseFinished仅在完成时触发——订阅三者会按设计收到同一条回复多次;
  • url仅允许http:/https:协议,javascript:/ftp:/裸字符串一律拒绝;
  • surveyIds是必填数组,空数组表示"所有问卷"(故不加min(1));?surveyIds[]=<id>会被接受但毫无效果(返回全部 webhook),故不过滤参数不声明;
  • webhooks.update全量替换而非补丁:v2 对整包重新校验,漏掉source就 422。早期版本恰好漏了它,导致该操作完全无法调用成功;现在所有字段必填,替换语义直接体现在签名里。

webhooks.delete之所以是 destructive,正如风险等级一节所述:重建订阅会签发新签名密钥,所有校验签名的接收端都要跟着改。它的镜像驱逐是强制的:一个存活的镜像行描述着一个账户认为自己已删除的集成,比"不整洁"严重得多。

客户端 API(client)与存储(storage)

客户端 API 是问卷 widget 使用的表面,工作区作用域写在路径上:

  • client.createDisplay:关联参数是userId而非contactId——传contactId会被接受(200)并忽略:展示记录以contactId: null落库且调用方毫无信号(已用真实联系人 id 验证)。此外问卷必须是inProgress状态,draft问卷返回403 "Survey is not accepting submissions",听起来像权限问题,实为问卷状态问题;
  • client.contactsState:目录描述为"获取客户端联系人状态",需要userId。四个 GET 形态(contacts/{userId}/stateuser/{userId}/state、两个版本的contacts/state)全部 404——读取该状态的唯一方式是 POST,而它会把联系人 upsert 出来:未见过的userId会被创建而非报缺失。这就是它被标为write而非read的原因(index.ts 注释详细记录了这次修正);
  • storage.uploadPublic/uploadPrivate两个操作是同一路由,仅accessType不同(POST v1/management/storage)。响应是 S3预签名 POST 授权——插件只负责拿到授权,传输字节是调用方自己的 multipart POST,目录文档化的 5MB 上限由 S3 在那一步强制执行,而非此处。早期版本曾把私有上传指向客户端路由,结果任何 body 都返回400 "Fields are missing or incorrectly formatted",该操作根本无法成功。

分页:Formbricks 对自己都不一致,插件如何统一

分页是这份集成里最容易被"200 掩盖"的坑。shared.ts 的PageStyle表按效果(播种至少三行后比较?limit=1?limit=1&offset=1?limit=1&skip=1的返回)鉴别了每条路由实际认可的游标参数:

路由limitoffsetskip
v1 management/surveys支持支持不支持
v1/v2 management/responses支持不支持支持
v2 management/webhooks支持不支持支持
v2 organizations/{id}/teams支持不支持支持
v2 organizations/{id}/workspace-teams支持不支持支持
v2 management/contact-attribute-keys支持不支持支持
v1 management/contacts不支持不支持不支持
v1 management/action-classes不支持不支持不支持
v1 management/contact-attributes不支持不支持不支持
v1 management/contact-attribute-keys不支持不支持不支持
v2 roles不支持不支持不支持

两个主动误导的陷阱被特别点名:

  1. meta会说谎。v2 列表返回meta: {total, limit, offset}——在忽略offset参数、只认skip的路由上报告一个offset字段。信信封,正是错误结论得以存活的原因;
  2. 四条 v1 路由连limit都忽略。不只是游标,连页大小都不认,永远返回全部行。这些操作因此不接受任何分页参数——声明一个会被提供商丢弃的输入字段,等于让插件自己误导调用方:设了limit: 10却拿到 4000 行,这是插件的错,不是 Formbricks 的。

插件的应对是:调用方面向的名字统一为offset(对可分页的操作),由listParams(style, input)一处把它翻译成对应路由实际认的 wire 参数。v1 的management/surveys是全 API 唯一认offset的路由——早期版本正是"只测了这一条路由"就在所有地方发offset,导致六个列表操作在返回 200 的同时分页错误。

limit被钳制在1–250,这是目录文档化的响应上限(API 自身不强制:?limit=1000也返回 200,故这是客户端侧防御,取文档值而非凭空设定;此前 100 的上限低于目录承诺)。不能分页的列表操作则干脆不声明分页参数——例如contacts.list是四条不可分页路由中最要紧的一条:联系人列表原则上无界且含个人数据,调用方有充分理由要一页,却没有任何办法拿到一页(客户端切片仍会传输全部行),该问题已向维护者上报而非在本地掩盖。

错误处理与重试策略

error-handlers.ts 注册的处理器几乎全部按 HTTP 状态匹配。由于 Corsair 按声明顺序取第一个匹配的处理器,而ApiError的 message 内嵌了响应体,所有消息嗅探型处理器都以"无状态"为前提门控hasNoStatus)——否则一个可重试的 500 若响应体恰好含 "not found" 就会被NOT_FOUND_ERROR截胡、永不重试。

处理器匹配行为
RATE_LIMIT_ERROR429,或无线程状态且消息含 "too many requests"最多重试 3 次,可带上retry-after;Formbricks 未文档化限流预算,这是防御性处理
AUTH_ERROR401不重试;日志提示检查 API key 及作用域(组织级 key 够不到工作区路由)
PERMISSION_ERROR403不重试;与 401 区分是因为修复方式不同:401 是 key 坏,403 是 key 没权限
NOT_FOUND_ERROR404不重试;Formbricks 只有一种 404 形态,无法从 body 区分"记录没了"与"路径错了",缓解措施是路径全为端点文件里的字面量并由路由测试兜底
VALIDATION_ERROR400 / 422不重试;describeValidationFailure能读出 v2 错误体details数组里的{field, issue},点名词条字段(v1 则读message
SERVER_ERROR500–599重试 3 次(非幂等操作除外);其中responses.updatedata的 500 是服务端 bug,已被 schema 前置拦截
NETWORK_ERROR无状态且消息含 network/connection/ECONNREFUSED 等按非幂等性决定重试 0 或 3 次
DEFAULT兜底不重试

非幂等集合(error-handlers.ts)是重试策略的核心:Corsair 请求重试时会整体重放整个端点packages/corsair/core/endpoints/bind.ts),网络失败若发生在 Formbricks 已提交写入之后,就会应用两次,而 Formbricks 不接受任何幂等键。因此以下操作被列入集合,重试次数为 0:surveys.createresponses.createactionClasses.createwebhooks.createcontacts.createcontacts.uploadBulkcontacts.updateAttributescontactAttributeKeys.createclient.createDisplayclient.createUserclient.identifyUserclient.contactsState。其中后四个按"注册表名"而非资源名命名(早期草稿写的displays.create之类根本不是注册操作,被endpoints.test.ts的漂移测试首次运行就全部抓住)。删除命名资源与更新操作有意缺席:删除重复执行不是重复风险(第二次报 not-found 恰好被删除流程读作"确认已不存在"),PUT对命名记录在效果上幂等。

数据镜像策略:配置镜像、数据不镜像

插件通过 Corsair 的数据库将部分实体镜像到本地(persist.ts 提供cacheEntities/cacheEntity),划分原则是configuration 与 collected data 的分界(schema/database.ts):

  • 镜像(配置):问卷、action class、webhook、联系属性键、团队——账户创作、极少变化,且是其他操作所需的查找表(回复引用surveyId、联系属性引用attributeKeyId);
  • 不镜像(收集数据):回复、联系人、联系属性、展示记录——从真实受访者持续流入的数据洪流,本地副本到达即过期,且那是别人的个人数据的副本。这一点在 Formbricks 上比多数服务商更尖锐:一条回复就是可识别个人的问卷答案,收集它正是产品本身的目的。

审计事件同样遵循此原则:联系人创建/更新的审计记录属性键名而非属性值(值可能是某人的邮箱和姓名),userId也从不入日志。

实时集成测试:如何跑、测什么

integration.test.ts 是针对真实 Formbricks Cloud 工作区的实况测试套件,默认运行被jest.config.cjstestPathIgnorePatterns排除、CI 命令行同样排除、无 key 时自跳(describeLive),因此不带凭据的检出跑全绿且不触网。运行方式:

FORMBRICKS_API_KEY=<key> pnpm test:live

关键约定:key 必须工作区级——组织级 key 在大多数管理路由上返回 401(v1/management/me会明说)。该套件会写入,且每次写入都在finally中清理;测试前后比对计数,不匹配即判失败而非悄悄收拾。不触碰的是存量记录:工作区预置的问卷只读不改不删,联系人清理只删本套件创建的 id/邮箱,绝不GET management/contacts后全量清空。

测试断言也印证了前面的实现事实:expectApiError匹配的是ApiError.body而非message——因为ApiError.message对 Corsair 已映射的状态是状态码的通用名(403 到达时是"Forbidden",不是 Formbricks 的"Survey is not accepting submissions"),对未映射的 422 则把 body 插值进字符串,对象 body 会渲染成字面量"[object Object]"

常见陷阱速查

最后把文档与源码中反复强调的坑汇总为速查表,供接入时对照:

  1. key 作用域:组织级 key 在绝大多数管理路由上 401,先用me.getGET /api/v2/me核验;
  2. workspaceId在 body:多数写操作要求它显式出现在请求体中(responses.update例外);
  3. 双版本:问卷/回复在 v1,webhook/组织/团队/批量上传/属性键写在 v2,别用默认版本;
  4. 分页参数名不一致:除surveys外全部认skip;四条 v1 列表根本不支持分页;
  5. contacts.list无界返回:一次调用返回工作区全部联系人(个人数据),且无分页替代方案;
  6. contacts.updateAttributesuserIdupsert:想"检查某人是否存在"会创建他;
  7. webhook 更新是全量替换:漏source必 422;
  8. responses.update必须带data:否则触发服务端 500 bug;
  9. surveys.update用 PUT:POST 到条目路由是 405;
  10. client.createDisplayuserId:传contactId被静默忽略;问卷须inProgress否则 403;
  11. 批量上传 ≤250 行且每行要email
  12. webhook 删除使签名密钥失效:所有接收端要跟着更新。

以上全部结论均可回到 packages/formbricks 的 README、端点源码与测试中逐一核对,插件许可证为 Apache-2.0,完整类型与示例可参阅 packages/formbricks/README.md 中指向的插件文档页。

【免费下载链接】corsairConnect your users to their apps项目地址: https://gitcode.com/GitHub_Trending/corsa/corsair

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

MATLAB解析Miniseed地震波形数据的完整指南

简介&#xff1a;本资源是一份面向地震数据处理初学者与MATLAB信号分析用户的实用工具脚本&#xff0c;聚焦于解决Miniseed格式地震波形数据在MATLAB环境中的读取与解析难题。Miniseed作为国际地震学界通用的标准数据格式&#xff0c;广泛应用于台网监测、科研分析与教学实验&a…

作者头像 李华
网站建设 2026/9/16 17:18:46

VidBee 界面语言切换:3 步快速切换 14 种语言的完整指南

VidBee 界面语言切换&#xff1a;3 步快速切换 14 种语言的完整指南 【免费下载链接】VidBee Download video and audio from YouTube , TikTok , Twitter , Instagram , Facebook , Twitch , Bilibili , and 1000 sites—or import local media. Create searchable transcript…

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

Title and authors of the Paper:

Title and authors of the Paper: 【免费下载链接】LifeOS ⛰️ The Life Operating System — an intent engineering platform that moves you from your current state to your ideal state, in life and work. 项目地址: https://gitcode.com/GitHub_Trending/pe/LifeOS …

作者头像 李华