Brandfine Docs
SDK

Analytics

Install the Brandfine analytics tracker on your site with @brandfine/client.

bf.analytics.install() puts the tracker for this workspace's Brandfine analytics site on the page. Every pageview lands in the CMS's SEO & Analytics view.

import { createBrandfineClient } from '@brandfine/client'

// In browser code, with the workspace's publishable key:
const bf = createBrandfineClient({ apiKey: 'brandfine_pk_…' })
await bf.analytics.install()
  • Use the publishable key in the browser (brandfine_pk_…). It is public by design: it can read the analytics setting and start the chat and booking widgets, nothing else. Never put the secret workspace key in browser code.
  • Brandfine decides on every load. With the publishable key, turning analytics on applies to the next page load; turning it off reaches visitors within a minute. Nothing to rebuild or redeploy.
  • Cookieless. The tracker sets no cookies and stores nothing on the visitor's device, so it needs no consent banner. Google Analytics is separate: see Google Analytics and consent.

API

install(opts?) → Promise<AnalyticsInstallResult>

Injects the tracker. With a publishable key and no config, it adds Brandfine's loader script without fetching anything first. Resolves to one of:

{ installed: true, tagUrl: string }               // publishable key: loader added
{ installed: true, websiteId: string }
{ installed: false, reason: 'disabled' }          // analytics off in CMS — no-op
{ installed: false, reason: 'ssr' }               // no document in scope
{ installed: false, reason: 'already-installed' } // marker tag exists

Throws BrandfineApiError on non-2xx responses other than the disabled case (which is a valid { enabled: false } body).

Options

OptionTypePurpose
config{ enabled: false } | { enabled: true; websiteId; scriptUrl }Skip the runtime fetch; inject immediately with caller-supplied config. Use this when you've baked the values in at build time.

getConfig() → Promise<AnalyticsConfig>

Lower-level helper that returns the raw config without touching the DOM. Useful when you want to inject the script yourself (framework-specific <Script> component, CSP nonces, etc.).

type AnalyticsConfig =
  | { enabled: false; gaMeasurementId?: string }
  | { enabled: true; websiteId: string; scriptUrl: string; gaMeasurementId?: string }

gaMeasurementId is present when your workspace's Google Analytics property was created through Brandfine and you opted into tag injection on the Google Analytics tab. install() never loads it: see Google Analytics and consent.

loadGoogleTag(opts?) → Promise<GoogleTagLoadResult>

Loads Google Analytics (gtag.js) for that Measurement ID. Call it only once the visitor has consented; see Google Analytics and consent. Takes the same { config } fast path as install().

type GoogleTagLoadResult =
  | { loaded: true; measurementId: string }
  | { loaded: false; reason: 'not-configured' | 'already-loaded' | 'ssr' }

'already-loaded' means a previous call or a hand-installed gtag is already on the page. It's skipped, because a second config double-counts sessions.

revokeGoogleTag() → void

Call it when the visitor withdraws consent. It switches analytics storage off for the tag loadGoogleTag() loaded, and deletes Google Analytics cookies (_ga, _ga_<id>, _gid, _gat) on this host and its parent domains. A later loadGoogleTag() switches it back on.

overview(opts?) → Promise<AnalyticsOverview>

Composed traffic report for the workspace — summary KPIs (visitors, pageviews, bounce rate, average visit duration, live visitor count), a bucketed timeseries for charting, top pages, referrer sources, visitor countries, and device classes — in one round-trip. This is what powers the WordPress plugin's wp-admin insights panel; use it to build your own dashboard widgets.

Call it server-side. The response is your site's traffic data — fetch it from your backend or build step and render the result, the same way you'd treat any reporting API. Don't call it from visitor-facing browser code.

const report = await bf.analytics.overview({ range: '30d' })

if (report.enabled && report.verified) {
  console.log(report.summary.visitors, 'visitors')
  // report.timeseries → [{ t, visitors, pageviews }, …]
  // report.topPages   → [{ path, views, visitors }, …]
  // report.sources    → [{ source, visitors }, …]  ('' = direct)
  // report.countries  → [{ country, visitors }, …] (ISO alpha-2)
  // report.devices    → [{ device, visitors }, …]  (desktop/mobile/…)
}

Options

OptionTypePurpose
range'24h' | '7d' | '30d' | '90d'Reporting window. Defaults to '7d'. Buckets are hourly for 24h, daily otherwise.

Response states

ShapeMeaning
{ enabled: false }Analytics has never been enabled for this workspace.
{ enabled: true, verified: false }Tracker provisioned but no pageview recorded yet.
Full payloadsummary + timeseries + topPages, ready to render.

Change fields (visitorsChange, bounceRateChange, …) are fractional deltas vs the immediately preceding window of the same length — 0.12 means +12%.

Install paths

// Next.js (app router) — client component
'use client'

import { useEffect } from 'react'
import { createBrandfineClient } from '@brandfine/client'

export function BrandfineAnalytics() {
  useEffect(() => {
    const bf = createBrandfineClient({
      apiKey: process.env.NEXT_PUBLIC_BRANDFINE_PUBLISHABLE_KEY!,
    })
    bf.analytics.install().catch(console.warn)
  }, [])
  return null
}

Drop <BrandfineAnalytics /> into your root layout once.

Build-time config

Resolve the config with the secret key at build or request time and inject the tracker from it. Prefer the publishable-key install above: with this path, turning analytics on or off only takes effect after your next deploy (or when your own cache expires), which is the usual cause of a workspace stuck on Waiting.

// Next.js (app router) — server component
// components/BrandfineAnalytics.tsx
import { createBrandfineClient } from '@brandfine/client'
import { BrandfineAnalyticsClient } from './BrandfineAnalyticsClient'

export async function BrandfineAnalytics() {
  const baseUrl = process.env.BRANDFINE_API_URL
  const apiKey = process.env.BRANDFINE_API_KEY
  if (!baseUrl || !apiKey) return null

  try {
    const bf = createBrandfineClient({ baseUrl, apiKey })
    const config = await bf.analytics.getConfig()
    return <BrandfineAnalyticsClient config={config} />
  } catch {
    return null
  }
}
// components/BrandfineAnalyticsClient.tsx — the tiny client island
'use client'

import { useEffect } from 'react'
import { createBrandfineClient, type AnalyticsConfig } from '@brandfine/client'

export function BrandfineAnalyticsClient({
  config,
}: {
  config: AnalyticsConfig
}) {
  useEffect(() => {
    // baseUrl/apiKey aren't used when `config` is provided.
    const bf = createBrandfineClient({ baseUrl: 'http://unused', apiKey: 'unused' })
    bf.analytics.install({ config }).catch(console.warn)
  }, [config])
  return null
}

Drop <BrandfineAnalytics /> into your root layout's <body> once.

Build-time config in Astro

Server-side fetch in the frontmatter; pass the config to a <script> block.

---
// src/components/BrandfineAnalytics.astro
import { createBrandfineClient } from '@brandfine/client'

const baseUrl = import.meta.env.BRANDFINE_API_URL
const apiKey = import.meta.env.BRANDFINE_API_KEY

let config = null
if (baseUrl && apiKey) {
  try {
    const bf = createBrandfineClient({ baseUrl, apiKey })
    config = await bf.analytics.getConfig()
  } catch {}
}
---

{config?.enabled && (
  <script
    defer
    is:inline
    src={config.scriptUrl}
    data-website-id={config.websiteId}
    data-brandfine-analytics={config.websiteId}
  />
)}

Brandfine analytics and Google Analytics are treated differently, because the law treats them differently.

Brandfine analyticsGoogle Analytics
CookiesNone; nothing stored on the device_ga cookies; data sent to Google
Consent in the EUNot neededRequired before it loads
Loaded byinstall()loadGoogleTag(), which you call after consent

Brandfine never asks for consent and never loads Google Analytics on its own. Your site's cookie banner or consent platform (your own, or one like Cookiebot or CookieYes) asks the visitor, stores the answer, and tells the SDK:

bf.analytics.install({ config }) // cookieless, no consent needed

// When your banner reports consent to analytics cookies: on Accept,
// and on every later page load while the stored answer is yes.
myBanner.onAccept(() => bf.analytics.loadGoogleTag({ config }))

// When the visitor withdraws consent.
myBanner.onWithdraw(() => bf.analytics.revokeGoogleTag())

Your banner should:

  • ask before Google Analytics loads, with Reject as easy as Accept (same size, same screen);
  • let the visitor change their mind at any time, for example from a "Cookie settings" link in the footer;
  • keep a record of the choice and ask again after a while (six to thirteen months is common);
  • name Google Analytics in your privacy policy.

What the SDK guarantees

  • install() never loads Google Analytics.
  • loadGoogleTag() loads it with Google Consent Mode set to analytics only: ad storage, ad user data, ad personalization and Google signals stay off. Brandfine provisions GA for measurement, not advertising.
  • revokeGoogleTag() switches analytics storage off and deletes GA's cookies.
  • A hand-installed gtag is never tagged twice.

On WordPress, the Brandfine plugin reads consent through the WP Consent API, which cookie banner plugins such as Complianz and CookieYes support. Without one, it never loads Google Analytics.

Idempotency

Every injected script gets a marker attribute:

<script defer src="…" data-website-id="…" data-brandfine-analytics="…"></script>

install() skips injection when it finds an existing tag with the same data-brandfine-analytics value, and the publishable-key loader skips when any Brandfine tracker is already on the page. Safe across:

  • React StrictMode double-invokes
  • SPA route changes that remount the bootstrap component
  • Multiple SDK clients pointing at the same workspace

SSR safety

If document is undefined (server render), install() returns { installed: false, reason: 'ssr' } and does nothing. Call it from useEffect / onMount / equivalent client-only hooks.

CORS

The /external/analytics-config endpoint inherits the permissive CORS configured on /external/* — any origin may call it with a valid X-Api-Key header. No special setup on the customer side.

Verifying installation

After a page on your site loads with the tracker injected, the CMS's SEO & Analytics tile for this workspace flips from Waiting to Live within ~5 seconds (it polls a verify endpoint every 5s while in the Waiting state). With the publishable-key install, the tile first says analytics is installed, before the first pageview arrives. If it stays there, the tracker script is likely blocked, for example by an ad blocker.

On this page