Tracking Log (Redirect Tracking)
Overview
POST /api/tracking is a public endpoint that records redirect-link click events. Whenever a
user taps a link routed through our tracking system — from a rich menu, a rich message, a
campaign, or a link embedded in a content page — the redirector issues a POST to this endpoint
to register the click.
This is the thinnest module in the project: no database, no Redis, and no validation whatsoever.
It accepts whatever arrives, wraps it into a payload, and pushes it onto the tracking_log queue
so that worker-go can parse it and persist the result.
:::warning Do not make this synchronous The code carries an explicit comment forbidding any change that would make this endpoint synchronous. Tracking is being rebuilt behind client-api, so the behaviour here must stay frozen exactly as it is. :::
Business Flow
- Accept
POST /api/tracking— no authentication required. - Flatten every request header into a map keyed by lowercase names; multi-value headers are
joined with
", ". This mirrors thereq.headersobject that Express hands to a NestJS controller. - Bind the body into a
map[string]any, deliberately discarding any binding error. An empty, malformed, or non-JSON body still yields a 200 response — the original implementation had no validation at all. - Spawn a goroutine guarded by
recover()and publish usingcontext.Background()with a 10-second timeout. - Return
200with{"code":"RES_SUCCESS_001","message":"OK"}immediately, without waiting for the publish to complete. - The payload delivered to the
tracking_logqueue is bare JSON in this shape:
{
"headers": { "...all request headers..." },
"body": { "...the submitted body..." },
"logEventType": "redirect"
}
- If publishing fails — including the case where AMQP is not configured and returns
amqp.ErrNotConfigured— the service only logsError processing tracking log. There is no retry and no DLQ, so the caller has no way of learning that the message was lost.
The key order in the JSON payload (headers, body, logEventType) matches the original NestJS
object spread exactly, because the downstream consumer parses by field name.
Key Files & Functions
| Method | Route | Auth | Handler |
|---|---|---|---|
| POST | /api/tracking | None | Handler.Track |
All source lives under internal/tracking/.
| File | Highlights |
|---|---|
internal/tracking/handler.go | Register(r, deps), NewHandler, Handler.Track, flattenHeaders |
internal/tracking/service.go | NewService(pub, exchange, queue), Service.ProcessTrackingLog(ctx, headers, body) |
internal/tracking/service.go | type redirectTrackingPayload, const logEventTypeRedirect = "redirect", interface publisher |
Relevant constants: resSuccess001 = "RES_SUCCESS_001" and publishTimeout = 10 * time.Second.
Connections to Other Services
- RabbitMQ — the
tracking_logqueue (envRABBITMQ_QUEUE_TRACKING_LOG) on theline_exchangeexchange, with the routing key equal to the queue name. See RabbitMQ Publisher & Topology for publishing details. - worker-go — consumes
tracking_log, unpacks the headers and body into thetrackingdatabase domain, and associates the record with a LINE user when one can be identified. - client-api — owns the next-generation tracking implementation that is gradually taking over, which is precisely why this endpoint's behaviour must not change.
- cms-api — the
tracking-linkmodule issues the tokens and links that ultimately call this endpoint, whiletracking-line-usersreads the results back. - No Postgres or Redis is touched on this path; Redis is used only by the shared rate limiter.