Skip to main content

Friend-add Tracking Campaign

Overview

This feature measures friend acquisition campaigns for a LINE OA. When a user taps a campaign link, the app records the visit along with the user's LINE identity, then routes them to the OA's add-friend screen. As a result, the CMS can report which campaign each new friend came from, and which sub-channel within that campaign via the referral value.

There are two entry routes: one that names the campaign token directly, and one that lets the app resolve the currently active campaign from the OA hash.

Business Flow

Route with an explicit campaign (/:hash/friend-track/:token)

  1. The app fetches the OA data from the hash and the campaign data from the campaign token.
  2. This page calls liff.init() directly rather than going through the shared hook, so it can control the sequencing itself. If the user is not logged in, they are sent to the LIFF URL when inside the LINE app, or the login function is called when outside it.
  3. The ID token is retrieved with up to three retries, and the access token is retrieved as a fallback. If neither is available, the app logs out and restarts login.
  4. The user profile is fetched for the display name and picture — supplementary data, not required.
  5. The visit is recorded via POST /friend-track/:token/visit, attaching both x-liff-token and x-liff-access-token, with the display name, picture URL, and referral value in the body.
  6. The next step depends on whether the response says the user is already a friend of the OA:
    • Already a friend — the screen says so and automatically opens the OA's chat room after 2 seconds, with a button to open it immediately for users who do not want to wait.
    • Not yet a friend — the user is sent straight to the OA's add-friend screen.
  7. The referral value is read both from the query parameter directly and from inside liff.state, since the original query parameters are lost during the login redirect.

Route without an explicit campaign (/:hash/friend-track)

This route is used when the LIFF app's endpoint is configured to point here. The overall flow is the same, with three differences:

  • The app first tries to read the campaign token out of liff.state.
  • If none is found, it calls GET /friend-track/by-hash/:hash to resolve that OA's active campaign.
  • After recording the visit it redirects straight to the add-friend screen, with no "already a friend" state.

Alternate entry point: the article CTA button

The CTA button shown on the article viewer stores the campaign token in sessionStorage and sends the user to that campaign's LIFF URL along with the referral value.

Using the liff.line.me domain rather than the line://app/ custom protocol is a deliberate choice: the latter does not deliver liff.state as a query parameter when opened from LINE's in-app browser, which would lose both the campaign and the referral information.

Key Screens & Components

Pages

  • The explicit-campaign page (src/app/[hash]/friend-track/[token]/page.tsx) manages every state: loading, processing, already a friend, redirecting, and error.
  • The token-less page (src/app/[hash]/friend-track/page.tsx) handles the case where a LIFF app points its endpoint at this path.

Hook and service

  • The campaign hook (src/hooks/use-friend-track.ts) has a notable responsibility: stripping the query string LIFF appends to the token before using it, handling both encoded and unencoded question marks.
  • The friend track service (src/service/friend-track.service.ts) exposes fetching a campaign by token, fetching a campaign by OA hash, and recording a visit.

CTA component

  • The floating CTA button (src/components/content-viewer/FriendTrackCta.tsx) appears on the article viewer with an attention-drawing animation.

Endpoints used

MethodPathReturns
GET/friend-track/:tokenCampaign data: name, description, the OA's bot id, and active status
GET/friend-track/by-hash/:hashThat OA's active campaign, including its token
POST/friend-track/:token/visitWhether the user is already a friend, with an accompanying message

Dependencies

  • @line/liff is called directly on these pages for init, login, ID token retrieval, access token retrieval, and profile fetching.
  • Two LINE deep link forms are used: the add-friend link and the OA chat room link.
  • Connects to the Article Viewer through the campaign data attached to an article.
  • The OA bot id used to build deep links comes from the campaign payload itself, not from the OA lookup endpoint.
  • Both the ID token and the access token are sent together, because the API checks the ID token first and falls back to the access token (see LINE Login via LIFF).

Backend Details (Client API)

Token verification differs from every other feature

  • The backend always checks the ID token first, against the OA's primary channel. If the OA has no LINE Login configured → 401 LINE Login not configured for this channel.
  • Without an ID token it falls back to the access token; with neither → 401 No authentication token provided. This is why the page sends both.
  • A deliberate difference from other features: this endpoint calls the raw verifier without going through the shared path that auto-provisions guests. So opening a campaign page never creates a new user record, unlike form or menu pages. That preserves legacy behaviour and has an important consequence for custom attributes (below).

The dedup rule — the subtlest part of this feature

Repeat visits are not blocked outright; there is a condition under which they are counted again:

  1. The backend finds the most recent visit event for the (campaign, user) pair.
  2. If one exists → it finds the most recent unfollow event for the (user, OA) pair and compares timestamps.
    • No unfollow after that visit → the same funnel, so it returns alreadyTracked: true without writing anything to the database.
    • An unfollow after the visit → a new funnel, so processing continues, because someone who unfollowed and came back should be counted again.

What the web app should take from this: alreadyTracked: true is not an error — it means the user is already in the funnel. Refreshing the page or re-tapping the link the same day returns it normally, and campaign figures stay uninflated.

What gets recorded, and the side effects

  • A visit event is recorded, with the display name and picture taken from the body first, falling back to the token's values. With neither, they are stored as null. Sending profile data in the body therefore makes CMS reports more readable but is not required.
  • An empty referral value is stored as null, not an empty string.
  • A trigger job is published best-effort so a worker can evaluate automation rules — failures are swallowed and never affect what the web app receives.
  • Custom attributes are merged onto the user when the campaign defines attributes, or when it defines a referral attribute key and the user actually supplied a referral.
    • Important: this merge only happens when a user record already exists; no new record is created. A user who has never interacted with the OA before (and thus has no record) receives no campaign attributes, even with a correct referral — a real edge case affecting campaign-driven segmentation.

Compensating events for users who are already friends

  • The backend checks whether the user is already a friend of the OA and returns that state for the web app to act on (send them to the add-friend page or to the chat room).
  • If they are already a friend, the backend also records a follow event itself (when one is missing). The reason: LINE does not fire a follow webhook for someone who is already a friend. Without this compensation, the campaign funnel would sit at the "visit" stage forever and the campaign would look like it produced nothing.
  • Real follow and unfollow events come from the separate webhook service — this endpoint writes only visit events and this one compensating follow event.

Details of the campaign lookup endpoints

  • GET /api/friend-track/by-hash/:hash resolves the OA from the hash without checking status or deletion at all (parity with the legacy system), which is looser than any other endpoint in the platform — a disabled or deleted OA still resolves.
  • However, with no active campaign it returns 404 No active campaign found for this LINE OA, and an unknown OA returns 404 LINE OA not found.
  • When an OA has several active campaigns, the most recent one is always chosen — so the token-less route automatically points at the newest campaign, worth knowing when creating a campaign to supersede an existing one.
  • GET /api/friend-track/:token also returns the LIFF ID and the OA hash, so the web app can start LIFF from this payload alone. A missing description comes back as null rather than an absent key.

Edge cases and cautions

  • The visit endpoint has a route-specific rate limit of 10 requests per 60 seconds per IP, stricter than the application-level limiter. Many users sharing one egress IP (event Wi-Fi where a campaign QR is handed out) can genuinely hit it — worth planning around for on-ground campaigns.
  • Every body field is optional and body parse errors are ignored entirely, since identity comes only from headers. A malformed body will not fail the request; it will silently drop the name, picture, and referral.
  • A deactivated campaign returns 404 Campaign not found even with a valid token — already-distributed links stop working the moment an admin disables the campaign.