Docs
Install the browser SDK, record your deploys from CI, and ask Claude what people did.
Quick start
Install the SDK
npm install @ffanalytics/sdkCall init once, as your app loads
import { init } from "@ffanalytics/sdk"; init({ key: "pk_..." });pk_...is your project key, on the Projects page once you sign in. It is safe to ship in browser code: it can send events but not read them. From here, page views, clicks and errors are recorded with no other code.Add
release: commitShato line up every event with the change that shipped it.Connect Claude Code
claude mcp add --transport http analyticsff \ https://mcp.analyticsff.com/mcpRun
/mcpin Claude Code to sign in, then ask about your product in plain words.
Browser SDK
init options
| Option | Default | |
|---|---|---|
key | required | Your project key. |
release | none | The commit SHA of the running build. Every event carries it, so Claude can line events up with the change that shipped them. |
autocapture | true | false turns off automatic events. { pageviews, clicks, errors } turns them off one at a time. |
replay | off | { sampleRate } turns on session replay. |
host | https://us.i.analyticsff.com | Where events are sent. |
flushIntervalMs | 5000 | How often queued events are sent. |
maxBatchSize | 20 | Events are sent right away once this many are queued. |
debug | false | Logs every event to the browser console. |
Calling init again returns the first instance, so it is safe in code that runs more than once.
Functions
import { identify, reset, track } from "@ffanalytics/sdk";
identify(user.id, { email: user.email, plan: "pro" });
track("league_created", { size: 12 });
// on logout
reset();| Function | |
|---|---|
track(event, properties?) | Records an event of your own. |
identify(userId, traits?) | Call after someone logs in. What they did before logging in joins their account. Pass email to see it in the people list. Safe to call on every page load: it only sends when the id changes. |
register(properties) | Adds these properties to every event from now on, automatic ones included. Kept in memory, so a page reload clears them. |
unregister(key) | Stops adding one registered property. |
reset() | Call on logout. The next person on this browser starts with a new anonymous id and a new session. |
flush() | Sends queued events now. The SDK already does this when the tab is hidden or closed. |
Automatic events
These are recorded as soon as init runs. Event names starting with $ are the SDK's own.
| Event | When | Properties |
|---|---|---|
$pageview | On load and on every page change, including navigation in single-page apps. | $title, $referrer |
$click | Clicks on links, buttons, role="button" and anything with data-ff-event. | $text, $label (for icon-only buttons), $tag, $selector, $id, $classes, $href |
$exception | Uncaught errors and unhandled promise rejections, up to 50 per page. | $message, $stack, $filename, $lineno, $colno |
$identify | When identify() is called with a new id. | $anon_distinct_id and your traits |
$recording | When a session starts being recorded, with replay on. | none |
Every event, yours included, also carries $pathname, $host, $screen_width, $screen_height, $viewport_width, $language, $user_agent, $lib, $lib_version, and utm_source, utm_medium, utm_campaign, utm_term, utm_content when the URL has them.
Sessions and storage
A session ends after 30 minutes with no events. The SDK keeps the visitor's id and current session in localStorage, under ff_distinct_id and ff_session.
What is never read
The SDK never reads what people type into inputs. A click on a submit button records the button's label and nothing else.
Naming clicks
<button data-ff-event="signup_cta" data-ff-plan="pro">
Start trial
</button>
<nav data-ff-ignore>
...
</nav>data-ff-eventnames a click. This button sendssignup_ctainstead of$click.- Any other
data-ff-*attribute becomes a property: this click carriesplan: "pro". data-ff-ignorestops clicks from being recorded anywhere inside that element. It does not hide anything from session replay; useff-blockfor that.
Session replay
Session replay records the page so a visit can be watched back. It is off until you turn it on.
init({ key: "pk_...", replay: { sampleRate: 0.25 } });sampleRateis the share of sessions to record, from 0 to 1. It defaults to 1. Each session is picked once, so it is recorded on every page or on none.- Everything typed into inputs is masked, always.
- Nothing is recorded while the tab is hidden.
- The recorder only downloads when replay is on, so it adds nothing to your page otherwise.
- Each recorded session adds a
$recordingevent to that person's timeline.
Hiding parts of the page
<p class="ff-mask">Shown as asterisks</p>
<div class="ff-block">Not recorded at all</div>ff-mask replaces the text inside with asterisks. ff-block records an empty box the same size instead of the element.
Deploys and CI
Send a deploy event after each deploy and it sits on the timeline next to what people did, so Claude can tell you whether a change moved the numbers.
FF_KEY=pk_... npx @ffanalytics/cli deploy --env productionGitHub Actions
Add a step after your deploy step, with your project key saved as a repository secret:
- name: Record deploy
run: npx @ffanalytics/cli deploy --env production
env:
FF_KEY: ${{ secrets.FF_KEY }}The command also picks up the branch, repository, commit message, who ran it, and a link to the Actions run. If sending fails it prints a warning and exits normally, so it never breaks a deploy.
Other events
The first word is the event name, so the same command records anything from a pipeline:
npx @ffanalytics/cli ci_run --prop status=success --prop duration_s=312| Flag | |
|---|---|
--env <name> | The environment you deployed to. |
--sha <sha> | The commit. Defaults to $GITHUB_SHA, then the current git commit. |
--source <source> | deploy for the deploy event, ci for anything else, unless you set it. |
--prop key=value | Adds a property. Repeat it for more. Numbers and true/false are sent as numbers and booleans. |
--key <pk_...> | Your project key, if FF_KEY is not set. |
--host <url> | Where the event is sent, if FF_HOST is not set. |
--strict | Exits with an error when sending fails. |
Asking Claude
claude mcp add --transport http analyticsff \
https://mcp.analyticsff.com/mcpRun /mcp in Claude Code to sign in. Claude then sees the projects your account owns. Ask the question you actually have:
- Did yesterday's deploy change how long people stay?
- Who signed up this week, and what did they do first?
- Which errors started after the last release?
- Pin a chart of daily signups to my dashboard.
What Claude can look up
- Your projects, and the events in each with counts
- The properties found on any event
- Any event over time, by hour, day or week, split by a property
- Everyone who used your product, most recent first
- One person's full timeline, grouped by session
- Your dashboards, which it can read, create and add to
Dashboards
Ask Claude to pin a chart, table or number to a dashboard. A tile keeps the question, not the numbers, so it runs again every time you open the page.
Ask for a written answer, like a nightly recap, and Claude saves it as text. It stays as written until you ask again, then Claude rewrites it for the new dates. Sign in at analyticsff.com to see your dashboards.
Sending events over HTTP
For events from your server, or anywhere the browser SDK does not run, POST them to the same address the SDK uses.
curl -X POST https://us.i.analyticsff.com -d '{
"key": "pk_...",
"events": [{
"event": "invoice_paid",
"source": "server",
"distinct_id": "user_123",
"properties": { "amount": 49 }
}]
}'| Field | |
|---|---|
event | Required. Up to 200 characters. |
distinct_id | Use the same id you pass to identify(), so the event lands on that person's timeline. |
ts | ISO time. Missing, or more than a day off, and the time it arrives is used instead. |
source | app, server, git, vcs, ci, deploy or agent. Anything else is stored as app. |
properties | Any JSON object under 32 KB. Larger ones are replaced with { "$truncated": true }. |
session_id, release, url | Optional, the same as the browser SDK sends. |
Up to 500 events and 1 MB per request. The reply is 202 with how many events were accepted and rejected, or 401 for a key that is not recognized.