Byline CMS
  • Inicio
  • Docs
  • Acerca de
Byline CMS
Ver en GitHub
  • Search & Document Extraction
  • Getting Started
    • Overview
    • CLI
    • Development environment and example application
    • Configuration
    • Upgrading from 3.21 to 4.x
    • Upgrading from 4.11 to 4.12
  • Why Byline
    • Overview
    • Mission & Vision
    • Content Management in the Time of AI
    • Byline for Collections
  • Key Architectural Decisions
    • Overview
    • Core Document Storage
    • Core Composition
    • Transactions
    • Path Grammar
    • Deployment Topologies
  • Collections
    • Overview
    • Fields
    • Blocks
    • Relationships
    • Document Trees
    • Document Paths
    • File / Media Uploads
    • Rich Text Editor
    • Collection Versioning
  • Reading & Delivery
    • Overview
    • Client SDK (@byline/client)
    • Routing & API
    • Transports
    • Markdown Export
    • MCP Server
    • Caching
  • Search
    • Overview
    • Configure search
    • Indexing and reindexing
    • Search API
    • Search provider contract
    • Portable multilingual search analysis
    • PostgreSQL and MySQL search providers
    • Attachment extraction for search
    • Native search engines and backend portability
    • Semantic discovery and institutional standards
  • Auth & Security
    • Overview
    • Authentication & Authorization
    • Auditability
  • Internationalization (i18n)
    • Overview
    • The host i18n system
    • Admin interface translations
    • Content locales
    • Administering content locales
  • Admin UI
    • Overview
    • UI Kit (@byline/ui)
    • Admin-config registration
    • Collection groups
  • API Reference
    • Overview
    • Configuration API
    • Collections API
    • Fields API
    • Client SDK API
  • Scheduling
    • Overview
    • Recurring tasks
    • Scheduled publication
  • Analytics
    • Overview
    • Analytics configuration
    • Analytics browser agent and consent
    • Analytics ingest and deployment
    • Analytics storage, rollups, and operations
  • Testing
  • Inicio

Analytics browser agent and consent

Companions:

  • Analytics — package boundaries and standard data behavior.
  • Analytics configuration — server runtime and adapter registration.
  • Ingest and deployment — implementing the endpoint selected here.
  • Caching — application and edge caching responsibilities.

@byline/analytics-agent is a dependency-free browser library. Its simplest installation is the same on any stack: install the package, place its standalone file in the application's static assets, and load it with a script tag. No React, TanStack, Vite plugin, or server route is required.

Every installation supplies an application-owned, same-origin endpoint. The path must begin with /, must not begin with //, and must not contain a fragment. There is no package-wide /api convention.

Install the standalone script

Install the package and copy the published browser artifact to the directory the application already serves as static files:

Run these commands from the application package root. In the reference application that is apps/webapp, and the destination is apps/webapp/public/b.js:

npm install @byline/analytics-agent
cp node_modules/@byline/analytics-agent/dist/b.js public/b.js

Then edit the HTML document template that owns the public page. This framework-neutral example uses src/document.html and expresses options as bounded data-* attributes:

<script
defer
src="/b.js"
data-endpoint="/telemetry/events"
data-cdn-hosts="cdn.example.com"
data-ignore-prefixes="/admin,/api,/_byline,/telemetry"
></script>

The public filename is the application's choice. The script URL may use an asset host, but data-endpoint remains relative to the page origin, not the script origin. The package also exposes the same file through its @byline/analytics-agent/standalone.js export for build tools that resolve package assets.

Byline's reference webapp commits public/b.js so its setup demonstrates this ordinary static-file pattern. A unit test compares that file with ANALYTICS_AGENT_SOURCE; upgrading the package fails the test until the checked-in artifact is refreshed.

Static-file response policy belongs to the application's host. In the reference webapp with Vite 8.2.2, a browser-shaped Sec-Fetch-Dest: script request receives 200 and text/javascript in development and preview. Development adds Cache-Control: no-cache; preview adds no Cache-Control. Direct Nitro start returns 200 and text/javascript; charset=utf-8 with validators but no Cache-Control. None of these three public-file responses adds X-Content-Type-Options; a deployment may add nosniff as a general security header at its direct server or proxy.

When the public filename is content-versioned, it can use immutable caching. When the application uses a stable filename, configure a bounded lifetime and mandatory revalidation at the static host or proxy so fixes reach returning browsers. Do not assume that copying the file also selects those headers.

Alternative: serve the source dynamically

A host that prefers not to copy the artifact can serve the exact standalone build through an application-owned route. A TanStack Start route can use the host convenience:

Create this route file. TanStack's [.] filename escape makes the public route literal /b.js rather than a route parameter:

apps/webapp/
└── src/
└── routes/
└── b[.]js.ts

Add the following code to apps/webapp/src/routes/b[.]js.ts:

import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/b.js')({
server: {
handlers: {
GET: async () => {
const { serveAnalyticsAgent } = await import(
'@byline/host-tanstack-start/integrations/serve-analytics-agent'
)
return serveAnalyticsAgent()
},
},
},
})

Other hosts can return ANALYTICS_AGENT_SOURCE from @byline/analytics-agent/source with their chosen response API. serveAnalyticsAgent() adds text/javascript, bounded mandatory-revalidation caching, and X-Content-Type-Options: nosniff; the application still owns and mounts the route.

A browser script request carries Sec-Fetch-Dest: script. In development, Vite uses that header to send the request into its module transform pipeline before a TanStack Start .js route can answer. A route-based application can respond earlier by placing bylineAnalyticsDev({ agentPath }) before tanstackStart():

Edit the route-based application's apps/webapp/vite.config.ts plugin list:

import { bylineAnalyticsDev } from '@byline/host-tanstack-start/vite'
plugins: [bylineAnalyticsDev({ agentPath: '/b.js' }), tanstackStart()]

This plugin is only a Vite-development compatibility helper for the dynamic route pattern. Production Nitro does not use Vite's development middleware. Applications serving the file from Vite's ordinary public directory do not need the plugin in any mode.

Bundle the browser API

An application may bundle the collector rather than load a standalone script:

Create an application-owned browser entry such as src/analytics.ts, then import that file from the application's normal client entry:

import { installAnalyticsAgent } from '@byline/analytics-agent'
const agent = installAnalyticsAgent({
endpoint: '/telemetry/events',
ignoredPathPrefixes: ['/admin', '/internal'],
})
// Stop listeners when the owning integration is disabled or unmounted.
agent.stop()

Calling installAnalyticsAgent() again while an installation is active returns the existing controller.

TanStack React helper

The optional TanStack component renders the standalone script and connects committed router locations to it:

In the reference application, edit the public layout at apps/webapp/src/routes/$lng/_frontend/route.tsx:

<AnalyticsAgent
src="/b.js"
endpoint="/telemetry/events"
ignoredPathPrefixes={['/admin', '/api', '/_byline', '/telemetry']}
/>

src defaults to /b.js as a host-helper convenience; endpoint is always explicit. The component dispatches navigation events only after the router commits a new location. When it unmounts, it sends the agent's stop signal so a consent owner can revoke collection without relying on removal of an already executed script element.

Browser behavior

On installation, the agent reports one page event. A module-level marker prevents the initial page from being counted again if the agent stops and restarts during the same document lifetime. The next committed navigation is still eligible.

For page events, the browser identity is the URL pathname. Consecutive events with the same identity are suppressed until a different identity is observed. Query-only and fragment-only changes are ignored by default. Setting countSearchChanges or countHashChanges includes those components in the browser identity, so a committed change can produce another view. The server always removes query strings and fragments during normalization, however, so these options increase the view count for the stored pathname; they do not create separate dashboard paths. For example, /docs?tab=api#request is stored as /docs.

Host adapters must report committed locations only. Link hover, preload, prefetch, failed navigation, and React remounts are not page views.

One delegated document listener reports a download when an anchor targets a configured CDN host or ends in a configured extension. It does not cancel or delay navigation.

Download events use the link's pathname, without its query or fragment. Every event includes the current document's document.referrer. In a client-side application that value remains the referrer of the original document while the router navigates, so several SPA page events can carry the same external referrer. Byline does not create a session or replace it with the preceding SPA path.

The body is text/plain JSON with only:

This is the on-wire request body produced by the agent, not a file the application edits:

{ "v": 1, "kind": "page", "path": "/example", "ref": "https://referrer.example/" }

The payload contains no client timestamp, site identifier, user agent, client address, country, cookie, or visitor identifier. The browser supplies ordinary request headers independently, and the server host resolves network facts at its own trust boundary.

The agent uses navigator.sendBeacon, falling back to fetch with keepalive. It never retries and never reads the response.

Consent is application-owned

Byline does not decide whether a deployment requires consent. A site owner can load the agent immediately under its chosen legal basis or place the component behind an existing consent system:

In a TanStack application based on the reference layout, place the consent boundary in apps/webapp/src/routes/$lng/_frontend/route.tsx around the same helper:

<Consent category="performance">
<AnalyticsAgent
src="/b.js"
endpoint="/telemetry/events"
ignoredPathPrefixes={['/admin', '/api', '/_byline', '/telemetry']}
/>
</Consent>

When consent is absent, the component is not rendered and the browser does not request the script. When consent is revoked and the component unmounts, the agent removes its navigation, click, and stop listeners.

The initial-page marker is module-scoped and survives a stop. If consent is revoked and granted again during the same page lifetime, reinstalling the script does not recount the page currently on screen. The next committed navigation is the next page event. This avoids turning consent changes or React remounts into duplicate views.

The agent never writes cookies or browser storage. It reads localStorage["byline-analytics-ignore"] only for the explicit per-browser exclusion control. That flag is origin-scoped.

Global Privacy Control is not treated as a general first-party analytics signal because the standard subsystem does not sell, share, or use data for cross-context targeted advertising. A deployment that changes those purposes must revisit GPC handling and its privacy notice before shipping.

AnteriorAnalytics configuration
SiguienteAnalytics ingest and deployment

En esta página

  • Install the standalone script
  • Alternative: serve the source dynamically
  • Bundle the browser API
  • TanStack React helper
  • Browser behavior
  • Consent is application-owned
Byline CMS

Construyendo el futuro de la gestión de contenidos, un commit a la vez.

Proyecto

  • Documentación
  • Hoja de ruta
  • Contribuir
  • Versiones

Comunidad

  • Discusiones en GitHub
  • Blog
  • Boletín

Avisos legales

  • Política de privacidad
  • Condiciones de uso
  • Cookies

© 2026 Infonomic Company Limited y colaboradores. Open source y hecho con ❤️ por la comunidad.