Skip to main content

Friends Track

Overview​

Friends Track measures how well you're driving friend-adds on a LINE OA. Each campaign carries its own token that forms an invite link. When someone opens the link and authenticates through LINE Login it counts as a visit; if they then add the OA as a friend it counts as a follow. The system reports conversion rate over time, along with the list of real people who came through.

Two further capabilities: (1) it writes attributes onto users who arrive via the link, and (2) it supports per-referrer sub-links — upload a list of reference IDs (sales-rep codes, branch codes, and so on) and the system builds a separate link per code, so you can see which person or channel brought the most people in.

Who uses it — marketing staff measuring friend-add channels (this module carries a real, enforced ModuleGate on the backend — see Access control).

User journey​

1. Review the campaign list​

Open Friends Track. The list is sorted newest-created first, with 2 filters — search (matches name OR token) and a status filter (Active / Inactive).

The table shows Name, Token (in monospace), Status, LIFF Visits, Follows, Conversion Rate % and Created date. Those three middle columns are lifetime totals with no date filter. Three buttons per row: Report / Edit / Delete.

  1. Click create and enter a campaign name (the only required field) — it must be unique within the same LINE OA.
  2. Set the token — type your own, click the generate button, or leave it blank for the backend to generate one (those last two produce different lengths — see Key details). The token must be unique across the whole system, not just within your OA.
  3. Toggle Active/Inactive — new campaigns default to Active. An inactive campaign can't accept new visits.
  4. Enter a description (optional) — an internal note only; it appears nowhere in the list and nowhere the end user can see.
  5. Attribute Configuration — add as many pairs as you like; each pair picks a key from the merge-tag list (pulled from User Attribute Setup, both standard and custom attributes) and a value to write. Both fields are required once you've added a row.
  6. Ref Attribute Key (optional) — the key that stores the sub-link's reference ID on the user. Its options come from a different source than step 5: straight from the Attribute Master schema, displayed as attribute set > label. The hint under the field tells you to create the attribute in Custom Attribute management first.
  7. Save. A confirmation dialog appears first, then the system saves and returns you to the list.

The "Reference IDs" card appears only in edit mode — the campaign has to be saved and have an id before you can do this.

  1. Click the template download link — you get refid_template.csv with a refId header and 3 sample rows (this file is built in the browser, not fetched from the server).
  2. Upload a CSV with one reference ID per line. A result message appears as soon as it finishes.
  3. Once the status reads completed, the Download links file button becomes clickable (it's disabled until then) — you get a CSV with the full link already assembled for every reference ID.
  4. Distribute each link to the relevant person or branch.

4. Review the results​

  1. Click the Report button from the list.
  2. Review the four stat cards — Total LIFF Visits, Total Follows, Conversion Rate, Unfollows.
  3. Review the "Daily Visits vs Follows" line chart (there's a date-range picker in the card header — but it doesn't work yet; see Things to watch out for).
  4. Scroll to the Users card listing everyone who came through this campaign — Display Name with profile picture, LINE User ID, Visit Date, Follow Date, a status tag (Visited / Followed / Unfollowed), and the Ref ID of the sub-link they arrived on. Paginated.

Key details verified against the source​

Confirmed from internal/modules/friendtrack/{controller,service,dto}.go in line-management-cms-api-go, internal/friendtrack/service.go in line-management-client-api-go (where events are actually recorded), and internal/friendtrack/service.go in line-management-worker-go:

  • The token from the "generate" button and the token the backend generates are different lengths — the frontend button builds one from a UUID v4 with the dashes stripped, taking the first 16 characters, giving a 16-character token. Leave the token field blank and save, and the backend generates one with crypto.randomBytes(16).toString('hex') — a 32-character token. Both work identically, but it means tokens in the system come in mixed lengths and the links you hand out aren't the same length.
  • A "visit" only counts for someone who authenticated with LINE, and counts once per person — the client API has to verify a LIFF ID token (or access token) before it will record a liff_visit event, so anyone who opens the link and closes it before logging in is never counted. And every figure counts with COUNT(DISTINCT line_user_id) — opening the same link ten times still reads as 1 (the system replies alreadyTracked: true). One exception: if the user unfollowed the OA after their previous visit, coming back counts as a new funnel and records another visit event.
  • People who are already friends get counted as a "follow" automatically — LINE fires no FOLLOW webhook for someone who is already a friend, so the system checks for itself whether the visitor is already following and, if so, writes a follow event for that campaign right away (when one doesn't already exist). The consequence: conversion rate can read 100% purely from people who were already friends, not from new ones. Always read that number alongside the context of who you handed the link to.
  • Campaign attribution is last-touch and scoped to the whole OA — when a FOLLOW webhook reaches the worker, it looks up that user's most recent liff_visit across the entire OA (not restricted to any one campaign) and credits the follow to that visit's campaign. So if someone clicks campaign A's link, then campaign B's, then adds the friend, campaign B takes all the credit. Unfollow events attach to the campaign of the user's most recent follow.
  • Automatic attribute writing only works for users who already exist in the system — the attribute-merge function guards on finding the user's line_user row and, when it doesn't, silently does nothing — no error, no warning log. So a genuinely new person who has never been a friend of this OA gets no attributes at the moment they click the link. The merge is also shallow: an attribute configured on the campaign overwrites any existing value the user had under the same key.
  • The real link is assembled server-side only, and comes in two shapes in the same file — the base form is {clientWebUrl}/{oaHash}/friend-track/{token}?ref={refId}. But if a Content page with status published is linked to this campaign (via the friend_track_campaign_id column), the CSV header switches from refId,link to refId,directLink,contentLink, where the third column points at the content page instead: {clientWebUrl}/{oaHash}/content/{slug or publicToken}?ref={refId}. That's why some campaigns yield a 2-column file and others a 3-column one.
  • Reference-ID upload finishes inside a single request — it is not a background job — cms-api inserts in batches of 1,000 with INSERT ... ON CONFLICT (campaign_id, ref_id) DO NOTHING and writes the completed status before replying. The processing status does exist in the code but is written and overwritten within that same request. The system does have a friend_track_ref_import queue and a worker consumer for it, but nothing in cms-api ever publishes onto that queue — it's an unused path.
  • The "inserted/skipped" figures shown after an upload are cumulative, not per-upload — after inserting, the system counts with SELECT COUNT(*) FROM friend_track_ref WHERE campaign_id = ?, which is every reference ID on the campaign, not the rows just added, then computes skipped = lines in the file − that cumulative total. So on a second upload of all-new codes, inserted reports the campaign-wide total and skipped can go negative. This is behavior deliberately preserved from the original system.
  • The CSV header row is only stripped when the first line matches one of three words — refid, ref_id or ref (case-insensitive). Any other header, e.g. reference_id, gets stored as a genuine reference ID.
  • Each person's status in the report table is computed live from event ordering — Unfollowed when the latest unfollow is newer than the latest follow, Followed when any follow event exists, otherwise Visited. And the reference ID in that column comes only from the visit event — if that person's visit event carried no reference ID, the column is blank even when the follow event has one.
  • Handing the event on to Workflow is best-effort — after recording the visit event, the system publishes a message onto the trigger queue so automation can pick it up, but a publish failure is only logged and swallowed; the request still succeeds. So a visit can be counted in the report while the Workflow wired to it never runs.
  • User Attribute Setup — the source for both dropdowns in the form, via two different routes: the "attribute to write" field pulls the merge-tag list (standard and custom), while the "reference-ID key" field reads the Attribute Master schema directly. The attributes written land in the user's record in Member database.
  • Workflow — the trigger_friend_track_event trigger node uses Friends Track events to start an automation, and the campaign dropdown in that node offers Active-status campaigns only.
  • Content management — link a published Content page to this campaign and the downloaded links file gains a content-page link column you can hand out instead of the direct link.
  • Segment audience — the attributes Friends Track writes can then be used as conditions to build target groups in that menu.

Things to watch out for​

  • The date-range picker on the report page has no effect yet — the frontend sends parameters named startDate/endDate but the backend only reads dateFrom/dateTo, so both are ignored. The stat cards and chart therefore always show lifetime totals no matter which range you pick. (Picking dates really does fire a fresh request; the result just comes back identical every time.)
  • The "User Type" column in the report table always shows - — the backend never returns a userType field in the user-list result.
  • There is no way to filter the report by reference ID from the UI — the backend supports a refId parameter, but the report page never sends it. You can see which code each person came from, but there's no on-screen breakdown per rep/branch; you have to export and summarize yourself.
  • Unfollows can exceed follows — the system won't record a duplicate follow event for the same campaign (it skips when one exists), but it records every unfollow. Someone who adds, removes, adds, removes keeps incrementing unfollows while follows stays at 1.
  • The list shows only the token, never the full URL — the real link is assembled server-side from clientWebUrl and the OA's hash. You have to download the links file to get a URL you can actually hand out.
  • Upload status is only checked on page open and right after an upload — there is no ongoing polling. If the download-links button still won't click, reload the page.
  • Uploading with no file attached returns a 500 with no error code — behavior deliberately preserved from the original system (the response is {"statusCode":500,"message":"Internal server error"}, without the code key this API normally includes).
  • Tokens must be unique across the whole system, not just your own OA — typing a short, guessable token (e.g. promo2026) can collide with another organization's campaign and get rejected with "Token already exists".

Access control (verified against the source)​

  • Frontend — the list, form and report pages are all wrapped in a PermissionGuard checking the VIEW action on the friend-track subject.
  • Backend — all 9 routes in this module are wrapped in a real, enforced ModuleGate("friend-track") (onemoby callers bypass it). If a platform admin closes this module for an organization, its users get a 403 immediately, even calling the API directly.
  • Action-level policy annotations (create/readAll/read/update/delete) are declared on every route but run through the same no-op middleware as the rest of the system — the real enforcement is the module-level ModuleGate.
  • Every read and write is scoped by the caller's token line_oa_id — including fetching a campaign by direct id. A user in one OA can't guess an id to read another OA's campaign.
  • DELETE /api/friend-track/:id is a soft delete (stamps deleted_date). A deleted campaign disappears from the list immediately, but its recorded events and reference IDs remain in the database.
  • This module has no Super Admin check and no hard-delete route at all.

Source file reference​

Frontend (cms-web-2026)

  • src/app/friend-track/page.tsx, form/page.tsx, report/page.tsx — all three page entry points and their permission guards
  • src/components/friend-track/list/friend-track-list.container.tsx — the table, filters and delete flow
  • src/components/friend-track/form/friend-track-form.container.tsx — payload assembly, frontend token generation, and stripping/restoring the custom. prefix on attribute keys
  • src/components/friend-track/form/friend-track-form.tsx — the reference-ID card (upload/download/status) and both attribute dropdown sources
  • src/components/friend-track/report/friend-track-report.container.tsx — the four stat cards, line chart and user table (where the date parameters are sent under the wrong names)
  • src/services/friend-track.service.ts — every endpoint the frontend calls

Backend (line-management-cms-api-go)

  • internal/modules/friendtrack/controller.go — all 9 routes, ModuleGate("friend-track"), multipart file reading
  • internal/modules/friendtrack/service.go — token generation, reference-ID upload/download, link assembly, all three report computations
  • internal/modules/friendtrack/dto.go — inbound validation rules and the parameter names actually supported (dateFrom/dateTo/refId)

Other backends involved

  • line-management-client-api-go/internal/friendtrack/service.go — where visit events are actually recorded, the dedup rule, attribute writing, and the auto-recorded follow for already-friend users
  • line-management-worker-go/internal/friendtrack/service.go — last-touch campaign attribution and recording follow/unfollow events from the webhook

See the documentation team's earlier technical write-up at Friend Add Tracking