Goals and server events

Count outcomes your server confirms, like purchases and signups, with @littlefriend/node or one HTTPS call, kept apart from clicks in the browser.

A goal is an outcome you care about: a signup, a purchase, a booked demo.

Two kinds of goal

  • Browser goals match an event or a page view, like a click on Buy or a visit to /thanks. They are quick to set up and good for intent.
  • Server-confirmed goals count only events your server sends with a secret key. Browser clicks with the same name never count toward them. Use these for money and anything else that must be true.

Each goal definition has a version number, so you can tell when a goal's meaning changed.

Match on event properties

An event goal can also require up to 3 of the event's properties to equal values you choose. A goal on signup.completed with plan equal to pro counts pro signups only. Every condition must hold.

The event your app sends
import { track } from '@littlefriend/tracker';

// After the signup really went through
track('signup.completed', { plan: 'pro', seats: 5 });
A goal that counts it
littlefriend goals create --name "Pro signup" --event signup.completed --prop plan=pro
  • In the dashboard, edit an event goal on the Goals page and choose Add a property. With the CLI, repeat --prop key=value for each condition. goals update with --prop replaces the conditions, and --no-props removes them.
  • Conditions read the properties Little Friend stored. A property the collector dropped never matches.
  • Keys are lowercase snake case, up to 32 characters, each used once. Values are up to 64 characters, with spaces at either end dropped. A value that looks like an email address, phone number or card number is refused, because no event can carry one.
  • Values compare as text: seats equal to 5 matches the number 5, and trial equal to true matches true. Capitals count, so Pro matches only Pro.
  • Page view goals take no conditions. An event goal without any matches on the event alone.
  • Changing the conditions makes a new version of the goal. Counts already made keep the definition they were made with. Session timelines, funnel goal steps and flows use the current one.

Send outcomes from your server

Create a server key in Settings, Keys. It starts with lfs_. Keep it in an environment variable on your server, never in a browser.

From Node.js

@littlefriend/node checks, batches and retries events for you. It needs Node 18.17 or later.

Terminal
pnpm add @littlefriend/node
Send an outcome
import { LittleFriend } from '@littlefriend/node';

const lf = new LittleFriend({ key: process.env.LF_SERVER_KEY! });

// After the payment really went through
lf.track({
  id: 'order_8812', // the same id on a resend counts once
  name: 'purchase',
  props: { plan: 'pro' },
  value: { amount: 4900, currency: 'USD' },
});

Events wait in memory and go out in batches. Call await lf.flush() before a serverless function returns, and await lf.shutdown() before a long-running process exits.

From any other language

Post a batch to /v1/server with your server key as a bearer token. A batch holds up to 50 events.

The batch
{
  "v": 1,
  "events": [
    {
      "id": "order_8812",
      "name": "purchase",
      "correlationId": "chk_4f1a9c2e7b3d8a61",
      "value": {
        "amount": 4900,
        "currency": "USD"
      },
      "props": {
        "plan": "pro"
      }
    }
  ]
}
Send it
// After the payment really went through
const res = await fetch('https://in.littlefriend.io/v1/server', {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.LF_SERVER_KEY}`,
    'content-type': 'application/json',
  },
  body: JSON.stringify(batch),
});

if (res.status === 429 || res.status === 503) {
  // Busy: wait Retry-After seconds, then send the same batch again
}

Event fields

FieldRequiredWhat it is
idYesYour unique id for this outcome, like an order id: 8 to 32 letters, digits, _ or -. Keep it the same when you retry. @littlefriend/node makes one if you leave it out.
nameYesThe event name. Same rules as custom events, like purchase or signup.confirmed.
atNoWhen it happened, as epoch milliseconds or an ISO string. Defaults to when we receive it, or to when you call track in @littlefriend/node.
routeNoThe page it relates to, if any.
correlationIdNoA shared id that links this outcome to a browser event, like a hash of the checkout id: 16 to 64 letters, digits, dashes or underscores.
sessionIdNoJourney mode only: the session id handed from the browser, if you pass it along.
propsNoUp to 8 properties, with the same limits as browser events.
valueNoMoney, in minor units: { amount: 4900, currency: "USD" } is $49.00.

Responses

StatusMeaning
202Stored. The body is {"accepted": n, "dropped": m}, sent only after the write commits.
429 or 503Busy. Wait for the number of seconds in Retry-After, then send the same batch again.
400Malformed. Don't retry: fix the request first.
401The key is missing, wrong or revoked. Don't retry until the key is fixed.
413The body is over 64 KiB. Split the batch.