Skip to main content

Friend Add Tracking

Overview

Friend Track is the module for running friend-acquisition campaigns. Each campaign owns a token that identifies its invitation link. Opening the link counts as a visit; completing the add-friend action counts as a follow. Both are recorded so the campaign can be reported on as a conversion funnel.

Two things distinguish this module:

  • It writes attributes onto users automatically. Each campaign can define a set of key-value pairs, and every user who arrives through its link has those values written into their custom attributes — making the source of each follower traceable afterwards.
  • It supports per-person links. A single campaign can spawn many sub-links (one per sales rep or branch, for example) by uploading a CSV of reference IDs and downloading the generated links back as CSV.

The module comprises three screens: the campaign list, the create/edit form, and the results report.

Business Flow

Campaign list

  1. Opening the list checks permissions and then loads campaigns sorted by most recently created.
  2. Two filters are available: a search box that queries as the user types, and an active/inactive status selector. Both reset the table to the first page.
  3. Columns are the row number, campaign name (which links into edit), token, status, visit count, follow count, conversion rate, and creation date.
  4. Each row offers three actions — view report, edit, and delete — with deletion requiring confirmation and reporting its outcome through a success or failure dialog.

Create and edit form

  1. The form derives its mode from the id parameter: absent means create, present means edit. Both modes share the same view-level permission guard.
  2. Two supporting datasets load alongside: the merge tag list from the template message module (used as the attribute key options), and the existing campaign record when editing.
  3. The form fields are:
    • Campaign name (required)
    • Status, a switch that defaults to active when creating
    • Token, with a button that generates a 16-character string. Leaving it blank lets the backend mint one instead.
    • Description
    • Attribute configuration, a repeatable list of key-value pairs where the key is picked from the merge tag list
    • Reference attribute key, chosen from the configured attribute list, specifying where the sub-link's reference ID should be stored
  4. Saving opens a confirmation dialog first, then assembles the payload: the key-value list collapses into a single object, the custom. prefix is stripped from keys to match the storage format, and the status switch becomes a string.
  5. Loading an existing campaign performs the inverse transformation — the object expands back into a list, and the custom. prefix is restored on any key that is not one of LINE's standard profile attributes.
  6. If the save fails and the server returns per-field errors, they are displayed inline beneath the relevant fields.

The reference ID card appears only in edit mode, since a campaign ID must already exist.

  1. The expected file is a CSV with one reference ID per row; a header row is optional. The screen shows a worked example of the format.
  2. The template download builds the sample file in the browser directly, without contacting the server.
  3. Uploading sends the file for background processing. On success the screen reports the job status and the number of records stored. All content validation happens server-side.
  4. After the upload the screen fetches the job status. Only once it reads as complete does the link download become available, along with counts of records inserted and skipped. Status is checked on page load and after upload only, so a job still in progress requires a manual page refresh.
  5. The download button retrieves a CSV containing the tracking link for every reference ID and saves it locally.
  6. The links handed to end users are assembled server-side and returned only in that file. This differs from Content Links and Menu Builder, which build their URLs in the browser — which is why the list column shows the token rather than a full URL.

Results report

  1. The report reads the campaign ID from the URL and loads two datasets: the campaign record (whose name becomes the page title) and the report data for the selected period.
  2. The date range picker sits on the chart card header and governs both the chart and the user table together. By default no range is applied, showing all data.
  3. Four stat cards display total visits, total follows, the conversion rate, and total unfollows.
  4. A daily line chart is drawn from real server-supplied time-series data, plotting visits and follows as two series. If the selected range contains no data, an explanatory message appears instead.
  5. A user table lists everyone who arrived through the campaign, with display name, LINE user ID, visit date, follow date, status, the reference ID of the sub-link they used, and user type. It paginates independently of the chart.

Lifecycle summary

  1. An administrator creates a campaign, receives a token, and configures the attributes to be written.
  2. If per-person links are needed, they return to edit mode, upload the reference ID list, wait for completion, and download the link file for distribution.
  3. End users open the link; the backend records the visit and writes the configured attributes and reference ID onto anyone who adds the account.
  4. The administrator returns to the report, selects a date range, and reads the summary figures, chart, and user list.
  5. When the campaign ends it can be deactivated or deleted.

Key Screens & Components

The three routes — the list (src/app/friend-track/page.tsx), the form (.../form/page.tsx), and the report (.../report/page.tsx), all behind the same permission guard.

List container (src/components/friend-track/list/friend-track-list.container.tsx) — table loading, filtering, and deletion with its result dialogs.

Form container (src/components/friend-track/form/friend-track-form.container.tsx) — loads and saves the campaign, and owns the attribute format conversion in both directions. The form component (friend-track-form.tsx) holds the entire UI, including the reference ID card.

Report container (src/components/friend-track/report/friend-track-report.container.tsx) — assembles the stat cards, line chart, and user table, reusing the chart registration from the dashboard module.

Shared service (src/services/friend-track.service.ts) — list, read, create, update, delete, report retrieval, reference ID upload, upload status polling, and link file download.

Core data shapes — a campaign record holds its name, token, description, status, the attribute set to write, the reference attribute key, and summary figures. The report response splits into three parts: summary figures, a daily time series for the chart, and a paginated user list.

Dependencies

  • Permissions — the subject friend-track is checked for the view action only. Every screen shares that guard, including the form, which can create, update, and upload.
  • Template Message — supplies the merge tag list used as attribute key options. If that service is unavailable the options come back empty.
  • Attribute Master — supplies the reference attribute key options. The attribute must exist beforehand, which the on-screen hint states explicitly.
  • Backend and webhook — visit counting, follow counting, and per-reference link generation all happen server-side. The CMS only configures and reads results.
  • Dashboard module — shares the same chart library registration.
  • Shared CMS components — the section header, confirmation dialog, result dialogs, the full-screen loading state, and the table scroll area are the same ones used across the product.

Backend Details (CMS API)

This feature spans three backend pieces: the friend-track campaign module (internal/modules/friendtrack/), the tracking-log user lookup module, and a tracking-link generator that exposes no HTTP routes of its own.

Required permissions

  • Every /api/friend-track route is genuinely wrapped in ModuleGate("friend-track"). If the organisation has not enabled the module, requests are rejected before reaching a handler.
  • The declared policies are finer-grained than what the UI checks: listing is readAll; read-one, report, download, and status are read; creation is create; reference upload is classified as update rather than create; and removal is delete. Since the UI only checks the view action, these distinctions only bite server-side.
  • Note: the friend-track policy is also reused as the metadata for GET /api/apps and PUT /api/apps/:appId, carried over from the previous system. That is a coupling the module name does not suggest, and it deserves care before permissions are reworked.

Bulk reference upload

  • POST /api/friend-track/:id/refs/upload accepts a multipart file and writes it with a batched SQL statement carrying a skip-on-duplicate clause, designed for thousands to tens of thousands of references — one per staff member, per branch, or per printed QR code.
  • Because deduplication is built in, re-uploading the same file creates no duplicates, which makes it a safe way to top up references incrementally.
  • GET /api/friend-track/:id/refs/status is polled for job state (running / done / failed), since the work does not finish inside a single request.
  • GET /api/friend-track/:id/refs/download streams the CSV of every link and reference directly into the response rather than buffering it in memory.

Reporting and counting

GET /api/friend-track/:id/report filters by date range and reference. One performance note worth knowing: the user count in the report is produced by fetching all rows and measuring the result length, not by a database count, carried over verbatim from the previous system. Campaigns with large user bases will therefore make the report slower and more memory-hungry than it should be.

Who actually records which reference a friend came from

The CMS API does not write this event. When someone taps the link and adds the OA as a friend, the webhook service receives the event from LINE and writes it to the friend_track_event table, bound to the campaign and reference used. The CMS only configures and reads. The consequence: if the webhook service has trouble, report numbers go missing with no error surfaced in the CMS.

Looking up users from the tracking log

POST /api/tracking-line-users is the single endpoint that goes from behaviour back to people — "who tapped this link?" — taking a list of tracking keys, querying tracking_line_users, and joining back to LINE user records.

  • Scope comes from the request context, so results are confined to the OA currently in play.
  • This endpoint only passes the global JwtAuth; it has no ModuleGate, unlike /api/friend-track.
  • Results are returned as found, without reshaping.
  • Caveat: if the request omits the tracking key list or sends it in the wrong shape, the backend returns a 500 with a generic error code rather than a 400 explaining what went wrong. The behaviour is deliberately preserved for parity with the previous system, so callers must validate the payload before sending.

The tracking-link generator lives in internal/modules/tracking/ and exposes no HTTP routes at all — it is an internal service that the rich menu and messaging modules call. Points worth knowing:

  • The mode is selected by the TRACKING_REDIRECT_MODE environment variable. builtin uses tokens the platform encrypts itself; any other value (the default being the legacy mode) hands off to an external redirect service.
  • In builtin mode the token carries its own payload, encrypted with AES-256-GCM and encoded URL-safely, so the token itself identifies whether it came from a rich menu or a lead-generation form without a database lookup.
  • When a user taps the link, the client-api service decrypts the token, records the event in tracking_line_users, and only then redirects to the destination.
  • Important warning: the encoder on the CMS API side and the decoder on the client-api side are pinned to match. If either side changes the token format, every link already distributed stops working immediately, with no recovery short of reissuing them.
  • Campaign links take a different path entirely, going through the worker's redirect service rather than this token mechanism.

Tables involved

friend_track_campaign, friend_track_event, tracking_token, tracking_line_users, line_user, and line_oa.