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 existsThrows BrandfineApiError on
non-2xx responses other than the disabled case (which is a valid
{ enabled: false } body).
Options
| Option | Type | Purpose |
|---|---|---|
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
| Option | Type | Purpose |
|---|---|---|
range | '24h' | '7d' | '30d' | '90d' | Reporting window. Defaults to '7d'. Buckets are hourly for 24h, daily otherwise. |
Response states
| Shape | Meaning |
|---|---|
{ enabled: false } | Analytics has never been enabled for this workspace. |
{ enabled: true, verified: false } | Tracker provisioned but no pageview recorded yet. |
| Full payload | summary + 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
}Build-time fetch (static sites — recommended for Next output: 'export' and Astro)
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).