HTTP 协议
不用 SDK 时,按这个格式直接 POST /sa。路径和参数与神策数据接收接口对齐:data、data_list、gzip。App、小程序、其它语言的服务端都可以这样上报。Web、Go、Java SDK 把同一批事件放进 JSON 正文,不再套一层 Base64。
请求
http
POST /sa?gzip=1 HTTP/1.1
Host: stats.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZXZzX3drX3h4eDo=
data_list=H4sIAAAAAAAA...| 项 | 规则 |
|---|---|
| 方法与路径 | POST /sa。SDK 的 endpoint 就是这个完整地址。像素是 GET /sa.gif |
data | 单条事件。标准 Base64。gzip=1 时先 gzip 再 Base64 |
data_list | 多条事件的 JSON 数组,编码方式与 data 相同。两个都给时拼在一起 |
gzip | 1 表示 data / data_list 经过 gzip |
| 鉴权 | Authorization: Basic base64(写入密钥 + ":"),或查询参数 key(token 同等)。例如 evs_wk_xxx: 编码后是上面那一串 |
| 不能加请求头时 | 把同一把密钥放进 JSON 的 writeKey,或查询参数 key。两处都写时必须相同 |
| JSON 正文 | Content-Type: application/json 时可以直接发下面的 {"batch":[...]},事件字段与 data_list 里的相同。可以再加 Content-Encoding: gzip |
| 大小 | 解码后不超过 1MB,超过返回 41301 |
| 条数 | 每批最多 100 条 |
project 查询参数不参与鉴权,写了也会被忽略。
请求体
外壳用 Segment HTTP API 的批量格式:
json
{
"sentAt": "2026-09-23T12:00:00.000Z",
"batch": [
{
"messageId": "6f1c0b3e-1c0a-4d2a-9e5a-0b0c0d0e0f10",
"type": "track",
"event": "signup",
"anonymousId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"userId": "",
"timestamp": "2026-09-23T11:59:58.000Z",
"context": {
"library": { "name": "my-app", "version": "1.0.0" },
"page": {
"url": "https://shop.example.com/pricing",
"path": "/pricing",
"title": "价格",
"referrer": "https://www.google.com/",
"search": ""
},
"screen": { "width": 1440, "height": 900 },
"locale": "zh-CN",
"timezone": "Asia/Shanghai",
"campaign": { "source": "newsletter", "medium": "email", "name": "spring", "term": "", "content": "" },
"userAgent": "Mozilla/5.0 ..."
},
"properties": { "plan": "pro" }
}
]
}| 字段 | 说明 |
|---|---|
sentAt | 发送时间,RFC3339 毫秒 UTC |
messageId | 每条事件一个 UUID,入队时生成一次,重试时沿用 |
type | 见下表 |
event | type=track 时必填 |
anonymousId | 设备或访客的稳定 ID |
userId | 登录用户 ID,没有就空字符串 |
timestamp | 事件发生的时间,RFC3339 毫秒 UTC |
context.library | 你的 SDK 名字和版本,会进 lib 列 |
context.page | 页面信息。有 URL 的事件才会受项目的域名白名单约束 |
context.campaign | 广告参数,进 UTM 五列 |
context.userAgent | 用来解析浏览器、系统、设备 |
properties | 事件属性。值只用字符串、数字、布尔,单条最大 16KB |
traits | identify 时的用户属性 |
有 userId 时 distinct_id 取它,否则取 anonymousId。两者不会自动合并。
神策字段也能收:distinct_id($is_login_id 为真时当作登录 ID,否则当作匿名 ID)、original_id、login_id、time(毫秒时间戳)、lib.$lib / lib.$lib_version(js 记成 web)。
type
type | 入库 | 计入次数 |
|---|---|---|
track | event_type=track | 计入。event 为 $pageview 时同时计入 PV |
page | 规范成 track + $pageview | 计入 PV |
identify | track_signup,event=$SignUp;带 traits 时另记一条 profile_set | 不计入 |
track_signup | 原样 | 不计入 |
profile_set / profile_set_once / profile_increment | 原样 | 不计入 |
响应
与其它接口共用信封,采集额外带 accepted:
json
{ "code": 0, "message": "ok", "data": { "accepted": 1 } }| 情况 | 结果 |
|---|---|
| 成功 | code = 0。accepted 是实际写入的条数 |
| 批内个别事件被过滤 | 仍然 code = 0,accepted 只数写入的条数,不说明原因(原因在接入检查里) |
| 全部被过滤 | 同样成功,accepted = 0 |
| 整包解析不了 | 40001 |
| 密钥错误、项目停用或关了采集 | 40101 |
| 引导没完成 | 40003 |
| 超过 1MB | 41301 |
| 采集队列顶满 | 42901 |
重试
- 成功是
code = 0。 - 网络失败、超时、429、500 及以上:按 200ms 起指数退避,最多 5 次;仍失败就留在本地下次再发。重试时
messageId不变。 - 400、401:不要重试,检查请求和密钥。
建议每批 20~100 条、几百毫秒发一次,不要每个事件发一个请求。
调试
请求头加 X-Evstats-Debug: 1,并且密钥正确时,响应的 data.events 会带回规范化之后的事件。这些事件同样会真实入库。
bash
curl -sS https://stats.example.com/sa \
-u 'evs_wk_xxx:' \
-H 'Content-Type: application/json' \
-H 'X-Evstats-Debug: 1' \
-d '{"batch":[{"messageId":"8b0e2a1c-3f4d-4e5a-9b6c-7d8e9f0a1b2c","type":"track","event":"app_open","anonymousId":"device-1","timestamp":"2026-09-23T12:00:00.000Z","context":{"library":{"name":"my-app","version":"1.0.0"}}}]}'跨域
浏览器里跨源上报时,预检 OPTIONS /sa 按项目的 allow_origins 回答:留空允许所有来源,填了就只允许列出的来源。
版本
采集协议是 v1,请求里不带版本号。之后只会增加可以忽略的新字段,旧客户端一直能用。