Web
Use the FounderHQ Web SDK.
Capture pageviews, clicks, and your own events from a browser, and tie them to a contact.
Before you start
You need a publishable key (fhq_pk_...) for the brand you want to
measure. See Getting started.
Install
With npm:
npm install @founderhq/eventsOr with a script tag. The script defines a global named founderhq.
<script src="https://i.getfounderhq.com/events.js"></script>
<script>
founderhq.init("fhq_pk_XXXX");
</script>Initialize
Call init once, as early as you can. The SDK starts automatic capture
from that point.
import { founderhq } from "@founderhq/events";
founderhq.init("fhq_pk_XXXX", {
person_profiles: "identified_only",
});With no remembered or explicit consent choice, the SDK starts granted and
records everything. A site that asks for consent first passes
consent_default: "pending": the default mode then records only cookieless
pageviews and pageleaves until you call consent("granted") after
acceptance.
founderhq is a shared client. Call createFounderHqClient() instead when
you need a second, independent client with a different write key. For the
same write key, reuse the first client; see
Using one SDK client across your site and packages.
Using one SDK client across your site and packages
Run one client per write key on a page. Two clients with the same key count every page twice, and they share the same storage keys.
When init succeeds, the client registers itself for its write key.
getFounderHqClient(writeKey) returns that client, or null when no client
uses the key yet:
import { getFounderHqClient } from "@founderhq/events";
getFounderHqClient("fhq_pk_XXXX")?.capture("newsletter.subscribed");This matters most for a package that adds analytics to one part of a site,
such as a blog. The package does not own the site's SDK setup, so it sends
through the site's client instead of starting its own. It works when the
site and the package bundle different copies or versions of the SDK: the
registry lives on globalThis under
Symbol.for("@founderhq/events/clients").
- The result has three methods:
capture,page, andregister. The client can come from a different SDK version, so the type (FounderHqSharedClient) keeps only the methods every version has. - Look the client up when you send, not once when your code loads. The site
can call
initlater than your code runs. In React, a child component's effects run before its parent's. registeradds a property to every later event on the whole site. To describe one page or one part of a page, use page<meta>tags anddata-fhq-prop-*instead.- The first client that calls
initwith a key keeps the entry, unless it is a fallback client (see below). A second client with the same key still starts and captures. It logs one warning that points togetFounderHqClient. destroy()andclose()remove the client from the registry.- On the server,
initregisters nothing andgetFounderHqClientreturnsnull.
If your package must start its own client because the site has none yet,
pass yield_to_site_client: true to init. When the site's client later
starts with the same write key, your client sends what it has queued, stops,
and hands over. The site's client keeps the same visitor and session, and it
logs no warning. It also continues the page that is open at that moment
instead of sending a second pageview for it, so every page is counted once.
If a client with the key is
already running, a client with this option does not start at all, and logs
nothing.
getFounderHqClient(writeKey, { includeFallback: false }) ignores such a
fallback client, so it tells you whether the site runs its own client.
A blog built with @founderhq/next-blog works this way: it reuses your
site's client, and starts its own only when there is none. See
Next.js blog.
Capture events
founderhq.capture("trial.started", { plan: "growth" });track is an alias for capture. Both queue the event and return at once.
The SDK sends it with the next batch after consent is granted; custom events
called while consent is pending or denied are ignored.
Awaiting readyForCapture() first tells you that stored state is loaded.
Use it when a test or a redirect must not race the queue.
Pageviews, pageleaves, sessions, semantic clicks, rage clicks, dead clicks, outbound clicks, scroll depth, and Core Web Vitals are captured for you. Turn each one off with its own option below.
Click capture stores semantic metadata and the clicked element's own label
(its direct text, such as "Start free trial"), so a click reads as what the
person pressed. It never stores input values, a child element's text, page
contents, or click coordinates. Set captureElementText: false to store no
label at all. Add
data-fhq-no-capture or the class fhq-no-capture to keep a subtree out
of capture entirely. Use data-fhq-prop-* to add top-level click properties
for goals and filters; see Adding properties to automatic events.
To capture clicks in one part of a page only, see
Capturing clicks in part of a page.
Call page() to send a pageview yourself, and screen(name, properties)
for an app-like screen view.
Pageview properties
Each pageview reads the page's title, language, canonical URL, and preferred color scheme at capture time:
| Property | Value |
|---|---|
$title | document.title, trimmed; the site-name suffix is kept |
$document_language | The HTML element's lang attribute |
$canonical_url | The canonical link's resolved href |
$prefers_color_scheme | "dark" or "light" from the browser's preference |
Empty titles and languages, missing canonical links, and color scheme when
matchMedia is unavailable are omitted. The matching pageleave keeps these
values from its pageview, even after a single-page navigation updates the
document.
The SDK also reads 19 click IDs from the page URL, such as gclid,
fbclid, msclkid, ttclid, and li_fat_id. The full list is in
Event taxonomy. Like utm_*, they keep their query key as the property name.
They are read per page, not saved as a first-touch value.
Pageviews in a single-page app
The SDK sends a pageview for each pushState, replaceState, and
popstate that changes the URL. A call that keeps the same URL sends
nothing. Next.js calls replaceState on every router commit, and
router.refresh() and revalidation do the same, so these are not counted
as new pages. A change to the hash alone (#pricing) is a new URL and
counts as a new page.
The SDK sends a pageleave when the tab goes to the background
($leave_reason: "hidden"), because a browser can close a background tab
without warning. If the visitor comes back to the tab, the page is not
finished. When they leave it, the SDK sends another pageleave with the
same $pageview_id and the whole $page_duration_ms. Use the longest
pageleave of each pageview.
A router often changes the URL before it shows the new page: during a
loading state, or on back and forward. So the SDK sends the pageleave of
the old page at once, but waits for the new page before it reads $title
and the page's meta tags. It sends the pageview when one of these happens
first:
<head>holds anfhq-page-pathtag for the new path. See Describe a page with meta tags.- The title changes, when the old page had no
fhq-page-pathtag. - One second passes.
The pageview keeps the time of the navigation. A second navigation, or the
visitor leaving, sends a waiting pageview at once, with what the page shows.
Your first pageview and your own page() calls do not wait.
Adding properties to automatic events
Use auto_properties for $pageview, $autocapture, $rageclick,
$dead_click, and $outbound_click:
founderhq.init("fhq_pk_XXXX", {
auto_properties: ({ pathname, element }) => ({
section: pathname.startsWith("/docs") ? "docs" : "website",
...(element ? { control: element.tagName.toLowerCase() } : {}),
}),
});The callback receives event, url, pathname, and element. element
is the clicked DOM element, or undefined for a pageview. Import the
FounderHqAutoPropertiesContext type if you define the callback separately.
It runs on the initial pageview, each pushState, replaceState, and
popstate navigation that changes the URL (when that pageview is sent),
every page() call, and each click-family event at capture time. Return an object, or null or undefined to add nothing.
If it throws, the event still sends and the SDK warns once per client.
Remote config updates keep your callback.
For clicks, you can also put properties directly on an element:
<section data-fhq-prop-product-id="starter">
<button data-fhq-prop-action="buy">Buy</button>
</section>The click gets top-level product_id: "starter" and action: "buy"
properties that you can use in goals and filters. The SDK checks the clicked
element and every ancestor through document.body. The nearest value wins.
Hyphens in keys become underscores; values stay strings. These attributes
also stay in the existing $elements metadata. The no-capture attribute
and class still suppress the whole subtree.
Describe a page with meta tags
For pageviews, a page can describe itself with
<meta name="fhq-prop-<key>"> tags:
<meta name="fhq-prop-blog-article" content="pricing-guide" />
<meta name="fhq-prop-blog-author" content="Jane Doe" />The pageview gets blog_article: "pricing-guide" and
blog_author: "Jane Doe". Hyphens in the key become underscores, and the
value is the content attribute. An empty or blank content adds nothing.
Use each name once: if a name repeats, the first tag wins. The tags apply
to pageviews only, not to clicks. The matching pageleave keeps them.
The SDK reads tags in <head> only. It reads them when it captures the
pageview, at the same moment it reads $title.
In a single-page app, also tell the SDK which page the tags belong to:
<meta name="fhq-page-path" content="/blog/pricing-guide" />
<meta name="fhq-prop-blog-article" content="pricing-guide" />When <head> has an fhq-page-path tag, the SDK uses the fhq-prop-*
tags only for a pageview whose path matches one of them. It compares the
path alone: the query and the hash are ignored, and a trailing slash does
not matter. The SDK does not send fhq-page-path as a property. Without
it, the tags apply to every pageview, which is right for a site that loads
each page in full.
This matters because the old page's tags can stay in the document after a client-side navigation:
- During a loading state, or on back and forward, the old page's tags stay
in
<head>until the new page renders. - React 19 moves a
<meta>that a component renders into<head>, but it leaves the server-rendered copy in<body>. That is why the SDK reads<head>only.
With fhq-page-path, the SDK also knows when the new page has rendered.
See Pageviews in a single-page app.
Next.js
Render the <meta> tags in the page component. Do not rely on
generateMetadata for them: Next.js streams its output, so it can arrive
after the pageview.
Order and limits
Later values win in this order: automatic properties → auto_properties
→ data-fhq-prop-* on clicks, or fhq-prop-* meta tags on pageviews →
explicit page(properties) values. A pageleave keeps its pageview's merged
properties without calling the callback again.
All three sources accept keys of 1–64 characters that do not start with
$. Values must be strings, finite numbers, or booleans. Strings are
trimmed and cut to 200 characters, without splitting an emoji or other
character in two; other values are dropped. Each event keeps up to 20
extra keys: nearest attributes (or meta tags, in document order) first,
then callback keys in object-entry order. Explicit page() and capture()
properties keep their existing behavior.
All three sources follow document metadata: captureContext: false or an
injected platformFactsProvider skips them. The callback runs only in the
browser, including cookieless mode. The cookieless allowlist still applies
afterward: pageviews can carry pathname, referring domain, the five UTM
keys, the page's language tag ($document_language), and platform. Ad click
IDs, title, canonical URL, color scheme, and arbitrary custom properties,
including meta tag properties, are excluded. Cookieless mode captures no
clicks.
auto_properties adds properties to these automatic events and receives
the clicked element. It cannot drop or rename an event. beforeSend runs
afterward and sees every finished event; it can change the event or return
null to drop it. Session starts, Web Vitals, identity changes, screen
views, and your own capture() calls do not call auto_properties.
Capturing clicks in part of a page
Add data-fhq-autocapture to a container to capture clicks inside it. This
works even when you pass autocapture: false to init, or when its
allowlists leave the container out.
<aside data-fhq-autocapture data-fhq-prop-area="blog-sidebar">
<a href="/blog/pricing-guide">Pricing guide</a>
</aside>A click on a control inside the container sends $autocapture. It also
sends $rageclick, $dead_click, and $outbound_click when they apply.
- The clicked control must be inside the container. A button that wraps the container does not count.
data-fhq-autocapture="false"turns it off for a nested part. The nearest attribute wins.data-fhq-no-capture, the classfhq-no-capture,urlBlocklist, andselectorBlockliststill win.capture_rageclicks,capture_dead_clicks,capture_outbound_clicks, andcaptureElementTextstill apply.data-fhq-prop-*andauto_propertiesapply as they do to other clicks.- Consent and opt-out still apply. Cookieless mode captures no clicks.
- The container overrides only the
autocaptureoption you pass toinit. A remote configautocapture: falseturns these clicks off too, and removes the ones still queued. mode: "pageviews"sends pageviews only, with or without the attribute. A package that wants these clicks needs a client in the default"events"mode.
With autocapture: false, the SDK still listens for clicks. A click
outside every container stops after one DOM lookup. The observer that
detects dead clicks starts only at the first click inside a container.
A dead click is a click that nothing answered within one second: no page change, no navigation, no form submit, no focus or state change, and no scroll. A table-of-contents button that only scrolls the page is not a dead click.
Name an event for people
An event name is an identifier: FounderHQ shows it exactly as you wrote it,
and never rewords it. Send $display_name when you want a label read
instead:
founderhq.capture("trial.started", {
$display_name: "Trial started",
plan: "growth",
});The label is remembered against the event name, so sending it once is enough, and a later event without it does not erase it. Send it again to change it. Keep it short: it is a row label, not a sentence.
This works from any SDK, including the Node one, because it is an ordinary event property.
Identify a contact
founderhq.identify("user_42", { email: "jane@acme.com" });Call this after consent is granted. identify never changes consent.
Everything captured after consent but before identify stays attached to the
same person.
Pass an account in the third argument to change identity and account
together:
founderhq.identify("user_42", { email: "jane@acme.com" }, {
account: { key: "workspace_123", properties: { plan: "growth" } },
});Update contact properties on their own with setPersonProperties(set, setOnce). With the default identified_only mode, changes made before
identify stay on the device and go out with the identify call.
Repeating identify for the same ID is safe: the first call records the
identification, later calls only update properties, and a call with
nothing new sends nothing. name and avatarUrl fill the contact's name
and photo; see Identity and contacts.
Call reset() on logout. It clears the identity, the account, and the
session. When the browser is registered for web push, reset() first
removes it from the contact who signs out.
Accounts
An account is the company or workspace a contact belongs to.
founderhq.setAccount("workspace_123");
founderhq.setAccountProperties({ plan: "growth", seats: 12 });
founderhq.clearAccount();setAccount also takes an object with key, properties, and
contextToken. Pass account to init when the very first event must
already carry it. See
Accounts and groups.
Consent
| You call | What happens |
|---|---|
optIn() | Capture is on, and the choice is remembered |
optOut() | Capture stops, and queued data is cleared |
consent("granted") | Same as optIn() |
consent("denied") | The SDK stops writing identity and stores nothing |
isOptedOut() | Returns true after optOut(), with opt_out_by_default, or when Do Not Track is honored. It is false while consent is pending or denied |
Set opt_out_by_default: true to start silent until the visitor agrees.
Set respect_dnt: true to stay off when the browser sends Do Not Track.
The SDK starts granted when there is no remembered or explicit choice.
Set consent_default: "pending" when your site asks for consent first,
or "denied" to start in the denied state. In both, the default mode
records only cookieless pageviews and pageleaves.
getDistinctId() and getSessionId() return null while consent is not
granted. See
Consent and privacy.
Checkout attribution
Attach these values to a Stripe or Dodo checkout so the payment keeps its attribution.
const metadata = founderhq.checkoutMetadata();
const clientReferenceId = await founderhq.paymentLinkToken();checkoutMetadata() returns fhq_anonymous_id and fhq_session_id, plus
fhq_account_key and fhq_account_context when an account is set.
paymentLinkToken() returns one opaque string for Stripe Payment Links and
Pricing Tables, or null when consent is not granted. See
Checkout metadata and payment links.
Web push
Register the browser so FounderHQ Sequences can send it a notification. Your site asks for the permission and hosts one service worker file. The SDK does the rest. The setup is on Web push.
The methods are registerWebPush, requestWebPushPermission,
setPushEnabled, dismissPushNotification, and unregisterPushToken.
founderhq.identify("user_42");
const result = await founderhq.registerWebPush({
applicationServerKey: "YOUR_PUBLIC_KEY",
});| Method | What it does |
|---|---|
registerWebPush({ applicationServerKey, serviceWorkerPath?, serviceWorkerScope?, serviceWorkerRegistration? }) | Subscribes the browser and stores the subscription. Returns a promise with { status, reason? } |
requestWebPushPermission(options) | Shows the browser's permission prompt, then does what registerWebPush does. Call it from a click on your own button. Same options and result |
setPushEnabled(enabled) | Your site's own push switch for this browser. false stops pushes; the browser stays registered |
dismissPushNotification(key) | Closes the notifications your FounderHQ pushes showed with that notification key (or message ID), in this browser. Returns a promise with how many it closed. See Replace or remove a notification |
unregisterPushToken() | Removes the browser from the contact, forgets the subscription, and ends that subscription in the browser. Returns a promise |
registerWebPush never shows the permission prompt. It does nothing until
Notification.permission is granted, so you can call it on every page
load. requestWebPushPermission shows the prompt: call it only from a click,
because a browser refuses a prompt that no click started. It does not ask
again when the person already allowed or blocked notifications. serviceWorkerPath defaults to /founderhq-push-sw.js.
The SDK does not replace a service worker your site already has. With no
serviceWorkerPath, it then returns failed with the reason
service_worker_exists. Load founderhq-push-sw.js from the first line of
your worker and give its path. Or give serviceWorkerRegistration when
your site registers the worker itself. See
If your site already has a service worker.
status is registered, not_identified, permission_not_granted,
unsupported, consent_required, or failed. The promise never rejects.
With failed, reason is one of these:
| Reason | Meaning |
|---|---|
not_initialized | You called it before init |
invalid_application_server_key | The key is not the public key of a web push connection |
service_worker_exists | Your site has its own service worker and you gave no serviceWorkerPath |
service_worker_register_failed | The worker file did not register or did not become active. Make sure that your site serves it |
subscribe_failed | The browser refused the subscription |
other_push_subscription | The worker already has a push subscription from another push product. The SDK does not end it. A worker has only one subscription, so the SDK cannot add its own |
invalid_push_endpoint | The subscription does not point at a known push service |
The SDK only ends a push subscription that it made. A subscription from another push product on your site always stays.
setPushEnabled is kept in the SDK's storage. A call before init, in
cookieless mode, or while consent is not granted does nothing. Call it
again after init and after consent is granted. Before a contact is
identified, the SDK stores the switch and sends it with the next
registration.
A browser is registered only for an identified contact. The SDK sends the
registration after identify and until reset(), and sends nothing for a
guest. It sends it one time in each session, and again when the
subscription, the contact, or setPushEnabled changes. A page load in a new
session sends it from the stored subscription, also when the page does not
call registerWebPush.
reset() removes the browser from the contact who signs out. It keeps the
subscription in the browser and clears the setPushEnabled switch. If the
browser is offline, the SDK keeps the removal and sends it first on the
next page load. If the same contact signs in again before that, the SDK
cancels the removal.
The SDK stores the subscription in the browser's storage, so web push
needs analytics consent. In cookieless mode, or while consent is not
granted, registerWebPush returns consent_required and stores and sends
nothing.
When a visitor refuses consent later (consent("denied")), the SDK clears
its storage and cannot send a removal. It ends its own push subscription in
the browser instead, so the browser gets no more pushes.
optOut() is different. It stops everything that the SDK sends and clears
its storage, but the browser stays subscribed and continues to get pushes.
Use it for a person who must not be in your analytics, such as your staff.
To stop pushes too, call unregisterPushToken() before optOut().
unregisterPushToken() also works without consent. It sends nothing then,
and the SDK has no stored subscription. It ends only a subscription that is
its own: one on the founderhq-push-sw.js worker, or one made with the
applicationServerKey that the page gave to registerWebPush on this page
load. So call registerWebPush first on that page; without consent it
returns consent_required and only remembers the key.
When a person opens a notification, the service worker adds fhq_push to
a link on your own site. The SDK reads it on page load, sends
$push_notification_opened, and removes it from the address bar. You write
no code for this. The open is counted only when the person is identified on
the page that opens.
Options
Pass these in the second argument to init.
| Option | Type | Default | What it does |
|---|---|---|---|
mode | "events" | "pageviews" | "events" | "pageviews" sends pageviews only, and drops every other event |
host | string | https://i.getfounderhq.com | Where events are sent |
flushAt | number | 20 | Sends a batch once this many events are queued |
flushIntervalMs | number | 5000 | Sends a batch on this timer |
maxQueueSize | number | 100 | Largest queue held on the device |
storage | adapter | false | browser storage | Your own storage adapter, or false to keep the queue in memory |
person_profiles | "identified_only" | "always" | "never" | "identified_only" | When contact property changes are applied |
opt_out_by_default | boolean | false | Starts opted out, so nothing is captured until optIn() |
respect_dnt | boolean | false | Stays off when the browser sends Do Not Track |
consent_default | "granted" | "pending" | "denied" | "granted" | The consent state on a first visit. A remembered answer wins over it |
cookieless_mode | "off" | "always" | "when_not_granted" | "on_reject" | "when_not_granted" | Chooses cookieless behavior before and after the consent choice |
autocapture | boolean | object | true | Click capture. The object takes urlAllowlist, urlBlocklist, selectorAllowlist, and selectorBlocklist. A data-fhq-autocapture container captures clicks even when you set this to false; a remote config false stops them too |
capture_pageview | boolean | true | Pageviews, including single-page navigations |
auto_properties | (context: FounderHqAutoPropertiesContext) => Record<string, unknown> | null | undefined | none | Adds properties to pageviews and click-family events; click attributes, page meta tags, and explicit page() properties win |
capture_pageleave | boolean | true | Page exits, with time on page |
capture_sessions | boolean | true | Session start events |
capture_rageclicks | boolean | true | Repeated clicks on the same control |
capture_dead_clicks | boolean | true | Clicks that change nothing |
capture_web_vitals | boolean | true | One Core Web Vitals event per page |
capture_scroll | boolean | true | Scroll depth on the page exit event |
capture_outbound_clicks | boolean | true | Clicks on links that leave your domain |
cross_subdomain | boolean | true | Keeps one visitor across www. and app. on your domain |
cookie_domain | string | discovered | Overrides the cookie domain the SDK finds |
remote_config | boolean | true | Reads capture settings you set in FounderHQ |
beforeSend | (event) => event | null | none | Runs after automatic properties; changes each finished event, or returns null to drop it |
account | string | object | null | none | Installs account context before the first event |
yield_to_site_client | boolean | false | For a package's fallback client: hands over to the site's client when it starts with the same key; does not start if one is running |
redactUrlParams | true | string[] | see below | Replaces the listed query values with [redacted] |
captureContext | boolean | true | Set false to skip browser context, document metadata, auto_properties, data-fhq-prop-*, and page meta tags |
tracingHeaders | boolean | string[] | false | Adds the session id to your own API calls, so backend events join the visit. true covers same-origin requests; pass origins for a separate API host |
context | object | none | Your own values, added to every event |
captureElementText | boolean | true | Set false to leave the text of clicked elements out |
autoFlush | boolean | true | Set false to send only when you call flush() |
queueTtlMs | number | 24 hours | How long an unsent event may wait before it is dropped |
compress | function | false | gzip | Set false to send batches uncompressed |
redactUrlParams redacts token, access_token, auth, code,
password, secret, and key even when you leave it out. Pass an array
to replace that list with your own.
tracingHeaders wraps fetch and XMLHttpRequest so every request to
your own origin carries x-founderhq-session-id. Your backend reads that
header and passes it to the Node SDK, and the events it sends then sit
inside the visit that caused them. See
Join backend events to a visit.
Methods
| Method | Returns | What it does |
|---|---|---|
init(key, options) | the client | Starts the SDK. Later calls do nothing |
capture(event, properties) | — | Queues one event |
track(event, properties) | — | Alias for capture |
readyForCapture() | Promise<void> | Resolves once stored state is loaded |
identify(distinctId, properties, options) | — | Ties the visitor to a contact |
setAccount(account) | — | Sets the active account |
clearAccount() | — | Removes the active account |
setAccountProperties(properties) | — | Updates the active account |
setPersonProperties(set, setOnce) | — | Updates contact properties |
screen(name, properties) | — | Records a screen view |
page(properties) | — | Records a pageview |
register(properties) | — | Adds properties to every later event |
registerOnce(properties) | — | Same, but keeps an existing value |
unregister(key) | — | Removes one registered property |
optIn() / optOut() | — | Turns capture on or off |
consent(state) | — | Sets "granted" or "denied" |
isOptedOut() | boolean | Tells you whether capture is switched off entirely |
getDistinctId() | string | null | The current identity |
getSessionId() | string | null | The current session |
checkoutMetadata() | object | Attribution fields for a checkout. An empty object until consent is granted |
paymentLinkToken() | Promise<string | null> | A reference for Stripe Payment Links |
reset() | — | Clears identity, account, and session. Removes a web push browser from the contact |
registerWebPush(options) | Promise<{ status, reason? }> | Subscribes the browser to web push |
setPushEnabled(enabled) | — | Turns pushes to this browser on or off |
dismissPushNotification(key) | Promise<number> | Closes the FounderHQ notifications with that key |
unregisterPushToken() | Promise<void> | Removes the browser from the contact and unsubscribes it |
flush() | Promise<boolean> | Sends queued events now |
close() | Promise<void> | Sends the page exit event, flushes, and stops |
destroy() | — | Stops the client and removes its listeners, without sending |
applyRemoteConfig(config) | Promise<void> | Applies capture settings you supply yourself |
getFounderHqClient(writeKey, options) is a separate export, not a method.
It returns the page's initialized client for that key, or null. Pass
{ includeFallback: false } to ignore a yield_to_site_client client. See
Using one SDK client across your site and packages.
Remote config
The SDK reads your capture settings from FounderHQ at startup, so you can turn capture on or off without a redeploy. It caches the last answer in the browser and uses it on the next load.
On a first visit the SDK waits up to 1.5 seconds for those settings before it arms automatic capture. A slow answer never costs you the landing pageview. Settings that arrive later re-arm capture, and remove queued events they disable.
Set remote_config: false to skip the request.
Next
Read the protocol reference for the wire format, the reserved event names, and the campaign properties the SDK reads from a URL.