短链接的转化跟踪:点击标识符、归因窗口与服务端事件
- conversions
- analytics
- attribution
- link-shortener
每一份链接报表都有一个天然的停止点,对多数团队来说,那个点就是点击。看板告诉你,某条链接上个月被打开了四千次,并按国家、设备、浏览器和来源拆得清清楚楚,分析到此为止。可是四千次点击描述的是你花了多少钱,不是你赚了多少钱。它是漏斗里便宜的那一半,也是截图里最好看的那一半。
转化跟踪把这段缺口补上:它把一个标识符从跳转发生的那一刻,一路带进最终记录订单的那套系统,再回传回来。这个想法说起来简单,做错也很容易,而且那些错法往往要过几个月才浮出水面——表现为对不上发票的收入报表。本文覆盖这个标识符、归因窗口以及它为什么是一项业务决策、服务端事件与浏览器事件的区别、钱为什么是整数、重复是怎么处理的,以及当转化发生在你无法控制的系统里时该怎么办。
点击是成本,不是结果
只报点击的做法之所以长盛不衰,是因为点击是最容易衡量的那件事。跳转发生在你自己控制的基础设施上,统计它不需要任何人配合,而下游的一切都发生在别处。我们那篇链接点击到底是怎么被跟踪的讲了单次点击事件能包含什么,简短的结论是:它知道点击发生了,对之后的事一无所知。
这个局限带来的是实打实的后果。两个投放位可以产生一模一样的点击量,而其中一个送来的是买家,另一个送来的是三秒就跳出的人。在点击上看着漂亮的国家拆分,换成收入可能整个反过来。按点击率判胜负的分流测试,经常选中标题更激进、结账完成率却更差的那个版本。在收入被挂到同样这些维度上之前,上面每一个判断都只是一个打扮成指标的猜测。
而要把它挂上去,只需要一样东西:一个能从跳转一路活到订单的值。
标识符就是整个设计
每一次跳转,worker 都会签发一个令牌,并用它做两件事。它以你指定的参数名把令牌追加到目标地址上,默认是 lp_cid;同时把同一个值写进你自己跳转域名下的第一方 cookie,有效期 90 天。
之所以存两份,是因为任何一份都可能丢失。目标站点会剥掉查询参数,有时是为了 URL 干净而刻意为之,有时只是它那边一次跳转的副作用。与此同时,今天点了链接、三天后直接手敲地址回来的访客,身上已经没有参数,却仍然带着 cookie。两份副本单看哪一份都不可靠;合在一起,才覆盖得住通往购买的大多数现实路径。
这个 cookie 是第一方的,并不是措辞上的技术细节。它由你自己的跳转域名设置——正是访客真正访问过的那个域名,而这恰恰是浏览器一直没有在移除的那一类 cookie。这也是为什么自定义域名不再只是品牌偏好,而成了衡量基础设施:在厂商共用的域名上,cookie 属于厂商。
令牌里装着什么
令牌带签名,而签名覆盖的不只是这次点击。工作区和合作伙伴标识符都是签名载荷的一部分,因此在某个客户的链接上签发的令牌,到了另一个客户的工作区会像伪造品一样被拒绝。对于同时替多个客户跑链接的人来说,这意味着一个客户的收入不会跑进另一个客户的报表——靠的是结构本身,而不是某个人记得去加的一道筛选。
令牌装的东西是刻意收窄的:点击时刻、链接、分流版本、国家、设备类别、流量来源,以及一个标记可疑流量的标志位。没有任何个人数据。这份清单同时也是收入这一列有用的原因,因为其中每一个字段都会变成一个维度,让你在数据分析里直接按它拆分收入,不必再去关联任何别的东西。
参数名和总开关都在工作区设置里,改动其中任何一项,都会立刻重写每一条链接的缓存配置。有一个后果值得提前规划:当套餐不含转化时,跳转服务会彻底停止签发标识符,而事后再打开,也不会为已经发生过的点击追补标识符。
归因窗口是一项业务决策
窗口指的是一次点击最多能有多老,还能被算作某笔转化的功劳。它是一项逐工作区的设置,也是本文里唯一一个应该被拿出来争论、而不是照单接受默认值的数字。
设得太短,你就会丢掉自己真真切切创造出来的收入。设得过分长,你就会把本来也会发生的购买记到链接头上,那比没用还糟,因为它是理直气壮地错。正确的选法是去测量那些你已经能追溯的订单,看它们从第一次接触到成交之间隔了多久:冲动型购买几分钟就完成,需要斟酌的消费类购买要几天,带审批环节的企业采购要几周。
这个设计要避开的失败模式,是无声的拒绝。晚于窗口到达的转化会被 attribution_expired 拒绝,这个错误码和 invalid_click_id 是分开的。这两种情况需要的修法完全不同,而一个对两者只收到同一个笼统错误的集成,会花上一整周去查错的那一头。把错误码记进日志,两者分开计数,并把 attribution_expired 的比例上升当成一个信号:你的窗口已经跟不上你的销售周期了。
两条入口的上限也不一样。cookie 只活 90 天,因此依赖它的浏览器侧上报不可能比这更长命。服务端上报存的是标识符本身,只受工作区窗口的约束。
两条入口,一套规则
两条路都走同一个服务,因此归因窗口、去重和对外投递的行为完全一致。不同的是每条路各自扛得住什么,以及各自要求你付出什么。
服务端事件
你的后端把标识符连同一个转化目标、一个订单标识符、一个金额和一种币种一起提交。
curl -X POST https://api.linkprofit.com/v1/conversions \
-H "Authorization: Bearer lp_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-10241" \
-d '{
"click_id": "1.eyJ1aWQiOiJ...",
"goal": "purchase",
"order_id": "10241",
"amount_cents": 4999,
"currency": "usd"
}'
这个密钥需要 conversions:write 权限,这也意味着这条路会让你付出浏览器那条路不需要付的代价:一份要签发、要保管、要轮换的凭证。作用域密钥怎么管理见 API 认证,并且把这个密钥留在服务端——一把对你收入数据有写权限的钥匙,不该出现在页面里。
换来的是一份任何广告拦截插件、脚本拦截插件、跟踪保护设置或 JavaScript 报错都压不掉的上报。它由你自己的基础设施发出,时机是你自己的系统认定这笔订单确实存在的那一刻,因此它反映的是风控检查之后的事实,而不是按钮被按下那个乐观的瞬间。
浏览器事件
worker 会从你自己的跳转域名上提供一小段脚本,由感谢页来调用它。
<script src="https://go.example.com/cv.js" defer></script>
<script>
window.lpConversion({ goal: 'purchase', orderId: '10241', amountCents: 4999, currency: 'usd' });
</script>
脚本会自己从 URL 或第一方 cookie 里读取标识符,因此不需要手工往里传任何东西。整条链路上没有任何第三方主机,这一点有双重意义:它既是一项隐私属性,也是一项白标属性,因为你客户的页面不会加载任何写着底层平台名字的东西。
代价就是客户端衡量一贯的那份代价。拦截插件、隐私模式、脚本加载失败,以及提前半秒关掉标签页的访客,都会抹掉一部分事件,而且损失并不均匀。技术型和注重隐私的受众压掉的比例,远高于消费者受众,因此只走浏览器这条路不只是少算,而是按人群不均匀地少算。
两条路怎么选
只要有服务端知道这笔订单,就走服务端那条路——凡是有钱易手的地方,几乎都是如此。浏览器那条路留给只存在于浏览器里的结果,或者你加不了服务端代码的场合:一个只能通过标签管理器改动的页面、一个托管平台上的注册、一个不属于你的客户落地页。
因为有去重,同一个结果同时跑两条路是安全的——但前提是两条路发的是同一个 order_id。做不到这一点,你增加的就不是覆盖率,而是被重复计算的收入。
转化目标本身很轻。一个目标就是一个有名字的结果,比如 purchase、signup 或 trial;它可以带一个默认金额和币种,供那些没带金额到达的转化使用;而一个在创建之前就被引用的目标,会在第一次被用到时自动创建。
钱是整数
金额一律是整数最小货币单位。在两位小数的币种里,4999 表示 49.99;而 49.99 这样的金额会被拒绝,并指明是哪个字段出错,而不是被悄悄舍入。
这件事会让人别扭大约一天,然后替他们省心好多年。二进制浮点无法精确表示大多数十进制小数,所以那个经典演示——两个金额相加,结果末尾冒出一个意料之外的数位——不是什么趣闻,而是收入列上每一次求和在规模化之后真实会发生的事。何况还有零小数位的币种,它们会打破「除以一百总是对的」这种想当然。
在边界上直接拒收小数,等于把唯一那次诚实的换算——从人读的价格换算成最小货币单位——挤进了唯一一个地方:你的代码里,只此一处,而且是你测得了的地方。
重复是数据库的问题
去重是一次原子插入,而不是先读后写。同一个 order_id 被反复投递,只会产生一条转化,并把它标记为重复后返回。
这个区别听着像学院派的讲究,其实不是。先查后插的两步之间有一道缝隙,而一个被没耐心的发送方重试的 Webhook,会把两次调用正好落进这道缝里。两次都看不到已有记录,于是两次都写了一条。收入报表凭空长出百分之十、事后谁也找不出多在哪里,就是这么来的——因为单看每一条记录都完全合法。
实用的规矩是:挑一个在你自己系统里既稳定又唯一的订单标识符,然后再也不要改它的生成方式。你自己的订单号通常就是对的;时间戳,或者任何会在重试时重新生成的东西,则恰恰是错的。再配上前面那个请求里的 Idempotency-Key 头,一次失败的调用就变成了一件可以重来的事,而不是一桩需要调查的事。
可疑流量是被标注,而不是被藏起来。分类器的判定就装在令牌里,因此一笔归因到数据中心或代理的转化,在被记录的那一刻就已标记为可疑,并在报表里自成一个分组。「我们拿到了两百笔转化」和「我们拿到了两百笔转化,其中四十笔在一小时内来自同一个主机托管地址段」是两件不同的事实,而只有其中一件值得掏钱。转化数据与流量过滤不再是两个彼此无关的功能,正是在这个地方。
当转化发生在别的地方
实践中的大部分难处并不在 API 上。难在值得衡量的那一刻发生在一个托管商城、一套 CRM 或一家计费服务商内部,而你的活儿,是把一个字符串从点击送进那套系统,再把它取回来。
托管商城。 如果平台允许自定义订单属性或元数据,那就是最干净的路子:访客落地时读出标识符,用一个隐藏字段带着它走完结账,把它存到订单上,等订单确认时——而不是按钮被按下时——由你的服务端提交这笔转化。如果平台不允许任何自定义字段,却允许在确认页上放一段脚本,那就改走浏览器这条路。这些取舍在我们的电商解决方案页里讲得更细。
CRM 与有销售参与的成交。 把标识符作为线索表单上的一个隐藏字段收下来,存到记录上,等这笔单子被标记为赢单时再发送转化。这里的陷阱是时间:一笔七周才成交的单子,需要一个容得下七周的窗口;而如果你的 CRM 是唯一知道这个标识符的地方,那么它的保留期和导出行为就成了你归因架构的一部分。可以考虑在收下线索时发一个 signup 目标、在成交时发一个 purchase 目标,这样即便漏斗底部要走一个季度,顶部也已经被衡量到了。
计费与订阅。 让计费服务商的 Webhook 来当触发器。一张账单被支付时,你的服务端查出存在这个客户名下的标识符,用账单标识符作为订单标识符提交一笔转化。这样一来,周期性扣费自然会以一笔笔独立的转化到达,而首次付款和续费之间的区别,靠转化目标就分得清。
这三种形态里的套路完全一样:外部系统根本不需要知道链接的存在。它只是保管一个不透明的字符串,然后在需要时把它还给你。
这些数字之后往哪里走,是刻意做得平平无奇的。一个带签名的 conversion.created 事件会通过常规的 Webhook 机制发出,而配置好的广告平台集成会经由一条投递队列收到这笔转化,队列带着写在文档里的退避策略和一个放弃标记,而不是无限重试。套餐还可以按自然月(以 UTC 计)限制被接受的转化数量,超过这个上限返回的是 quota_exceeded,而不是套餐限制类的错误,于是「你的套餐里没有这项」和「这个月的额度用完了」不用开工单就能分清。
一次就把它做对
- 在营销活动开始之前就打开转化,而不是之后。标识符是在跳转那一刻签发的,无法事后追补。
- 把链接跑在你自己的域名上,好让第一方 cookie 属于你自己。
- 按你实测出来的购买周期设置窗口,并在
attribution_expired的比例发生变化时重新审视它。 - 只要有服务端知道这笔订单,就优先走服务端那条路。
- 把价格换算成整数最小货币单位这件事,在代码里只放一个地方做。
- 用你真实的订单号作为订单标识符,并且从每条路发出的都完全一致。
- 把各类拒绝码分开记录,每个周期与计费系统核对一次,遇到差距去调查,而不是用平均值把它抹平。
完整的字段说明见转化文档,而转化跟踪功能页讲的是收入这一列在你本来就在看的那些报表里如何呈现。这一切都不是要取代记账。它回答的是一个记账回答不了的、更窄的问题:是哪条链接、哪个国家、哪个目标地址、哪个流量来源,带来了这笔钱。
大家常问的问题
点击标识符到底是什么,它又存在哪里?
它是跳转服务在跳转发生的那一刻签发的一个带签名的令牌。它会以你指定的参数名追加到目标地址上,默认为 lp_cid,同一个值还会写进你自己跳转域名下的第一方 cookie,有效期 90 天。之所以存两份,是因为任何一份都可能丢失:目标站点剥掉查询参数时,cookie 还在;几天之后才回来、地址栏里早已没有参数的访客,身上仍然带着它。令牌带签名,并且绑定到签发它的那个工作区和合作伙伴,因此在某个客户的链接上签发的令牌,到了另一个客户的工作区,会像伪造品一样被拒绝。
在没有第三方 cookie 的情况下,转化跟踪还能用吗?
能用,因为整条链路里根本没有第三方。标识符走在目标 URL 里,而备份用的 cookie 由你自己的跳转域名设置,而不是由某个平台主机设置。从感谢页上报转化的那段浏览器脚本,同样由你自己的跳转域名提供,所以页面不会加载任何属于别家公司的东西。浏览器一直在移除的,是由访客从未访问过的主机设置的那种 cookie——而这里压根没有用到那套机制。
金额为什么要用整数分提交,而不是小数?
因为在支付链路中途临时发明一条舍入规则,正是收入报表悄无声息地对不上发票的原因。金额一律是整数最小货币单位:分、便士、戈比。49.99 这样的金额会被拒绝,并指明是哪个字段出错,而不是被悄悄四舍五入成一个看起来合理的数。浮点算术无法精确表示大多数十进制小数,成千上万笔金额加起来就会漂移,而这种漂移一直看不见——直到财务来问,为什么看板和账本差了几百个单位。
如果我的系统把同一笔订单上报了两次,会怎么样?
只会记录一条转化,第二次调用会把同一条转化标记为重复后返回。去重是一次原子插入,而不是先读后写,因此即使两次投递在同一时刻从一个被重试的 Webhook 抵达,它依然成立。正是这一点让重试变得安全:一个无法判断上一次调用是否成功的集成,只要用同一个订单标识符再发一次就行。
归因窗口应该怎么选?
靠测量你真实的购买周期到底有多长,而不是照抄某个广告平台的数字。去看那些真实订单从第一次接触到成交之间隔了多久:冲动型购买几分钟就完成,需要斟酌的购买要一周,企业级的单子要一个季度。窗口比真实周期短,你就会丢掉本来赚到的收入;窗口长得离谱,你就会把跟链接毫无关系的购买也记到链接头上。晚于窗口到达的转化会被 attribution_expired 拒绝,这个错误码和 invalid_click_id 是分开的,好让你的集成不必去猜,就能分清「太晚了」和「标识符坏了」。
发生在我无法控制的商城或 CRM 里的转化,也能归因吗?
通常可以,前提是那套系统允许你在订单或记录上多存一个字符串。访客到达时从 URL 或第一方 cookie 里取出标识符,把它放进一个隐藏表单字段或一个自定义订单属性,等订单确认时再由你的服务端回传。如果平台完全不允许自定义字段、却允许在确认页上放一段脚本,那就改走浏览器这条路。唯一没有干净解法的情况,是既不接受自定义数据、也不允许脚本的系统——这时候诚实的选择只剩下重做结账流程,或者为每条链接单独做落地页。