通过AI Agent结合MCP协议实现Shopify webhook自动化调试,捕获byte-exact请求数据并智能定位集成问题。
客户家的 Shopify 店铺把订单发给代发厂家。这个集成有两个问题,本来只打算修一个,结果顺便把两个都修好了。
任务是修取消订单的功能。取消订单的集成本来在设计里,为了赶 deadline 砍掉了——这种事在座的开发者都干过,不止一次。取消订单靠电话来,而且很少见,厂家也prefer这样,所以这个缺口就这么拖着,整整两年。没人告诉我发生了什么,我也没问。整整两年风平浪静,然后突然来了一个请求,这件事本身就是一种答案。
系统里本来就内置了一个三十分钟的延迟,在订单发往厂家之前。设计这个延迟就是为了这事:在通知厂家之前留出时间让订单可以被取消。窗口是有的。只是应用没有在监听取消事件。
客户取消了订单,Shopify 记录了,应用还以为订单是活跃的,到后面东西该做做、该发货发货。没人发现问题,直到客户问为什么没收到退款,或者为什么东西还是寄到了。
第二个问题更响,至少对我来说。当 Shopify 拒绝一个 fulfillment 时,系统会发邮件给真人求助。这部分是故意的:又是 scope 和 timing 的问题,不得不先搁置的时候,把活儿交给人是个合理的止损点。
问题出在更细节的地方:错误行从来不清理,所以下一次运行时还是找到同一个未解决的行,继续发同样的邮件,就这么一直循环,直到有人手动在数据库上执行了一条更新。
这份工作换来了时间,把这事儿彻底搞定:通知一次,然后自我修复。
调试之前的管道说明
在动手之前,先解释一下整体的管道是怎么接的。这篇文章后面会反复用到几个词,双方理解一致会顺畅很多。FlurryPORT 采用了以下术语来描述它的产品:
Capture。一次记录的投递:到达的所有内容,逐字节存储。
Endpoint。你的 capture URL,发送方连接的目标稳定地址。
Target。一个 capture 的下一站:FlurryPORT 之外的任意 HTTP 地址。
Replay。一个 capture 被投送到 target。
Auto-forward。没有人触发的 replay,因为 endpoint 配置为 capture 到达时就立即转发。
Watch。对一个 endpoint 的持续过滤器。每个到达的 capture 都会被检查,匹配的会带上它的标签。
FlurryPORT 完整术语表可以在这里找到。
测试 webhook 需要搭建一条管道。我把开发用的 Shopify app 指向了一个 FlurryPORT endpoint,专门用来 capture 我关心的那些 topic。Shopify app 的 TOML 里定义的 topic 需要一个绝对 URL 来通信,而 endpoint 正好提供这个。不需要动 app 的 URL,也不需要在会话之间维持一个 tunnel。
我在管道上设了三个 watch,每个对应客户 app 关心的一个 topic。定义 watch 过滤器用的是 JSONata 谓词来检查入站请求。这种情况下用 x-shopify-topic 请求头就足够路由管道流量了。取消订单到达时带了标签,可以随时 replay 到 localhost。
这就是整个测试框架:一个 endpoint 加三个 watch。按目前的限制刚好够用免费版。
FlurryPORT 可以在 endpoint 这一层验证 HMAC 签名。我没开,因为没必要。Shopify 把签名放在请求头里,那个头跟着 capture 一起走,底层的 payload 是逐字节存储和传输的。所以验证本来就可以在的地方进行:在客户的 localhost 后端,对着已经躺在那里的那份 secret。
Webhook secret 从没离开过 localhost。要是我想让 endpoint 拒绝一切非 Shopify 签名的内容,FlurryPORT 就需要一份副本。不需要。应用本来就配置好了要验证逐字节一致的 Shopify 请求。我的基础设施把它们原封不动地送达:这是我第一次用它接客户的活儿,而不是控制性演示。
所以下一部分才让人有点尴尬。
我要调试的那个 bug
第一次 replay 全挂了。
我做好了花一个下午调试的准备,结果我的 AI agent 读完 receipt 当场给出了答案。
FlurryPORT CLI 通过你发行的 PAT 来认证。flurryport login fp_... 保存了 token,供 AI agent 通过 CLI 的 MCP 实现来发请求。作为预防措施,token 的标准设置会脱敏 PII,即隐私范围。这个范围会尽最大努力遮盖个人数据。订单 payload 充满了看起来像个人数据的东西,因为本来就满是个人数据。
也就是说,到达 localhost 的字节不是 Shopify 签名的那份字节,所以验证理所当然地失败了,客户的 backend 返回了 401。
我来这里本来是调试 webhook 集成的。症状是 webhook 消费方返回 401。这根本不是 FlurryPORT 形状的错误,恰恰就是我会在客户代码里翻找的那种错误,而且我会发现客户代码没问题——因为客户代码本来就没问题。是我的工具的问题,只是披着我客户的 bug 的外衣。
Receipt 一直知道怎么回事。每次 replay 都会返回一个 receipt,上面带着一个标志,说明 payload 在传出时是否被脱敏了。它就放在那里,每次失败的尝试都标着。这事儿根本不需要推理,只需要读,而 AI agent 二话不说就要了一个升级过的 token。
我重新发了 PAT,关掉了 masking 范围,之后每一个 replay 都验证通过了。
我下了一个真实订单,取消了它,两个 capture 都进了我的 FlurryPORT 管道。
给我的 AI agent 简单描述了一下我们要解决的问题,然后就开始跑了。我的 agent 开始工作,往应用里发、重发那些 capture,再现问题并验证修复。要单元测试,它就又抓了一遍 capture 来构建逐字节一致的 stub,跑进测试套件。
整条路径都需要重新验证,做了这么多年养成的本能反应是立刻而且几乎是身体上的:去店铺里点一遍,放一个订单,取消它。
我没那么做。不需要那么做:FlurryPORT 还留着那些 capture,等着把活儿干完。
订单 #1090 的取消 capture 被回收来做集成测试。本地测试没问题,但我还是会至少再起一个额外环境来 catch 在开发机器上无法复现的问题。我不需要重新配置开发用 Shopify 店铺指向托管环境。改了 target,用同一份 #1090 的 capture 完成了集成测试。
总结一下这一天,那些 capture 跑了三个独立场景。接单时的 happy path。三十分钟窗口内,取消订单。超过三十分钟,订单已发给厂家,走升级邮件路径。然后是一套单元测试,fixture 匹配 Shopify payload。
没有下第二个订单。Webhook capture 的优化得到了验证。
一些关于测试的琐碎记录
代码库周日早上没有任何测试。当天晚上有三十个,一次跑全绿,到周末结束时有七十三个。
真实的 payload 教的东西手写 mock 永远教不了。取消订单后面的 update 带着 financial_status: "refunded",我不会想到去 mock 这个,mock 了也会写错。薄薄的取消 payload 明确不能反序列化为创建或更新,这听起来像 trivia,但直到你意识到这是供应商白送的一个免费类型检查,才明白它的价值。我花了一些时间出于兴趣读那些 payload。Webhook body 里沉淀了大量的决策过程,只要你慢下来去看。
这份工作让我建了什么
这些都不是产品规划。我没有坐下来设计什么。开发 session 发给我了模板。我为了调试这个集成搭的配置原来是三个 recipe 的雏形,第二天我认真写好了。
三个,全部在线上。capture-fixture-suite 把 capture 来的供应商流量变成一套测试套件,fixture 可以证明它们的字节从何而来。vendor-submission-tap 把应用的外发调用引流到一个 capture URL,这样你就能精确读取你发给供应商什么,而不用真的发出去。vendor-callback-simulator 把管道反过来,把一个 capture 来的提交重塑成供应商自己的回调形状,投送到你的应用,所以返回这条路也测试得到,不需要对方有人在。
三个加在一起刚好塞进免费版,一点点空余都没有。
我建 FlurryPORT 是基于一个信念:一个 capture 下来的请求值得保留。直到有了一份真正的工作,有真正的 deadline,另一头是别人的客户,才把这个信念变成了三样你今天下午就能装上的东西。
如果这些听起来像你某个周末的亲身经历,walkthrough 在 flurryport.io/recipes,flurryport.dev/try 大概两分钟,真实数字在 flurryport.io/pricing。