Skip to main content

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

  1. Accept POST /api/tracking — no authentication required.
  2. Flatten every request header into a map keyed by lowercase names; multi-value headers are joined with ", ". This mirrors the req.headers object that Express hands to a NestJS controller.
  3. 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.
  4. Spawn a goroutine guarded by recover() and publish using context.Background() with a 10-second timeout.
  5. Return 200 with {"code":"RES_SUCCESS_001","message":"OK"} immediately, without waiting for the publish to complete.
  6. The payload delivered to the tracking_log queue is bare JSON in this shape:
{
"headers": { "...all request headers..." },
"body": { "...the submitted body..." },
"logEventType": "redirect"
}
  1. If publishing fails — including the case where AMQP is not configured and returns amqp.ErrNotConfigured — the service only logs Error processing tracking log. There is no retry and no DLQ, so the caller has no way of learning that the message was lost.
note

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

MethodRouteAuthHandler
POST/api/trackingNoneHandler.Track

All source lives under internal/tracking/.

FileHighlights
internal/tracking/handler.goRegister(r, deps), NewHandler, Handler.Track, flattenHeaders
internal/tracking/service.goNewService(pub, exchange, queue), Service.ProcessTrackingLog(ctx, headers, body)
internal/tracking/service.gotype redirectTrackingPayload, const logEventTypeRedirect = "redirect", interface publisher

Relevant constants: resSuccess001 = "RES_SUCCESS_001" and publishTimeout = 10 * time.Second.

Connections to Other Services

  • RabbitMQ — the tracking_log queue (env RABBITMQ_QUEUE_TRACKING_LOG) on the line_exchange exchange, 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 the tracking database 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-link module issues the tokens and links that ultimately call this endpoint, while tracking-line-users reads the results back.
  • No Postgres or Redis is touched on this path; Redis is used only by the shared rate limiter.