Brandfine Docs
SDK

Analytics

Auto-inject the Brandfine analytics tracker into your site. Two install paths — runtime (any framework) and build-time (static sites).

bf.analytics injects the tracker script for this workspace's Brandfine analytics site. No manual <script> tag, no separate dashboard — every pageview lands in the CMS's SEO & Analytics view automatically.

import { createBrandfineClient } from '@brandfine/client'

const bf = createBrandfineClient({
  baseUrl: 'https://api.brandfine.co',
  apiKey: process.env.BRANDFINE_API_KEY!,
})

await bf.analytics.install()

That single call fetches this workspace's tracker config from Brandfine, then injects <script defer src="…" data-website-id="…"> into document.head exactly once. Safe to call on every page load — the SDK marks the injected tag and skips on subsequent calls.

API

install(opts?)Promise<AnalyticsInstallResult>

Injects the tracker. Resolves to one of:

{ 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. When present, install() also loads the Google tag (gtag.js) for that Measurement ID — idempotently, and never when another gtag setup already exists on the page (a hand-installed GA is not double-tagged). The Google tag sets cookies: consent banners remain your site's responsibility.

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%.

Two install paths

Pick based on whether your site is dynamic or static-exported.

Runtime fetch (dynamic sites)

The default install() call. Every page load makes one HTTP request to /external/analytics-config before the tracker can inject.

Wins:

  • Reflects enable/disable + re-provisioning instantly without a redeploy.

Costs:

  • One extra HTTP round-trip per page load before the tracker fires.
  • API key has to be shipped to the browser (a workspace read key, scoped to public-facing endpoints only — but still bundled).
// Next.js (app router) — client component
'use client'

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

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

Resolve the config server-side at build/request time and pass it to the client injector via props. The API key never reaches the browser bundle, and there's zero round-trip on page load.

Wins:

  • Faster: tracker injects immediately, no blocking HTTP request.
  • API key stays server-only.
  • Works even if the Brandfine API is briefly down.

Costs:

  • Enable/disable + re-provision changes take effect only on the consumer's next deploy.
// 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.

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}
  />
)}

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. 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).

On this page