@behindthescenes/analytics 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,63 +1,21 @@
1
1
  # `@behindthescenes/analytics`
2
2
 
3
- Browser SDK for BTS external-site analytics.
3
+ Browser SDK for BTS external-site analytics. Track visitor behavior, capture conversion events, and maintain attribution across the customer journey.
4
4
 
5
- ## What it does
5
+ ## Overview
6
6
 
7
- - Records standard web events such as `page_view`, `view_content`, `lead`, `sign_up`, `begin_checkout`, and `purchase`
8
- - Auto-tracks pageviews, SPA history changes, outbound clicks, button clicks, form submissions, safe GET search forms, and opt-in content views by default
9
- - Supports custom client-side events with `track(...)`
10
- - Preserves journey tokens, UTM params, and click IDs for downstream attribution and CAPI delivery
7
+ The BTS Analytics SDK provides:
11
8
 
12
- ## Auto-tracked events
9
+ - **Automatic event tracking** - Page views, clicks, form submissions, and content views captured without code changes
10
+ - **Conversion tracking** - Standard e-commerce and lead events (purchase, checkout, sign_up, lead)
11
+ - **Cross-site attribution** - Journey tokens preserve visitor context across domains
12
+ - **Attribution preservation** - UTM parameters and click IDs captured and persisted
13
+ - **GoHighLevel integration** - Contact form submissions directly to GHL
14
+ - **Google Analytics compatibility** - Detects existing GA installations or loads tags in proxy mode
13
15
 
14
- The SDK automatically captures events that can be detected safely from browser behavior:
16
+ ## Installation
15
17
 
16
- - `page_view`: initial page load and SPA navigation
17
- - `outbound_click`: links to external origins
18
- - `button_click`: clicks on `button`, `[role="button"]`, or `[data-bts-track-click]`
19
- - `form_submit`: form submissions
20
- - `search`: GET search forms with common query keys (`q`, `query`, `search`)
21
- - `view_content`: elements that opt in with `data-bts-view-content`
22
-
23
- Opt into automatic content-view tracking by marking elements that represent content cards, offers, posts, videos, or products:
24
-
25
- ```html
26
- <article
27
- data-bts-view-content="offer_123"
28
- data-bts-content-type="offer"
29
- data-bts-content-title="Creator Accelerator"
30
- >
31
- Creator Accelerator
32
- </article>
33
- ```
34
-
35
- Identity, purchases, signups, payment steps, and arbitrary custom events are not auto-detected because they need product context. Call those explicitly after your app knows what happened.
36
-
37
- ```ts
38
- // analytics.identify("user_123", { email: "fan@example.com" });
39
- // analytics.track("watch_preview_clicked", { placement: "hero" });
40
- // analytics.trackStandard("lead", { formId: "newsletter" });
41
- // analytics.trackStandard("sign_up", { method: "email" });
42
- // analytics.trackStandard("begin_checkout", { checkoutId: "offer_123" });
43
- // analytics.trackStandard("add_payment_info", { checkoutId: "offer_123" });
44
- // analytics.trackStandard("purchase", { orderId: "order_123", monetaryValue: 149 });
45
- // const { journeyToken } = await analytics.startFunnel("/landing");
46
- // const checkoutUrl = analytics.decorateUrl("https://behindthescenes.com/checkout", journeyToken);
47
- // await analytics.flushNow();
48
- ```
49
-
50
- ## Auto pageviews and SPA navigation
51
-
52
- - `**autoPageviews: false**` turns off the initial `page_view` **and** disables SPA history hooks (`pushState` / `replaceState` / `popstate`), so route changes do not emit automatic `page_view` events.
53
- - To track SPA navigations without the first-load pageview, pass an `autoTrack` object, for example `{ pageviews: false, history: true, outboundLinks: true, buttonClicks: true, formSubmissions: true, search: true, viewContent: true }`.
54
-
55
- ## Flush reliability
56
-
57
- - If a batch flush fails (network error or non-2xx response), events are **put back on the queue** and a later flush is scheduled (including the keepalive path used when `requestHeaders` is set).
58
- - The default `sendBeacon` unload path cannot attach custom headers or observe success; use `requestHeaders` if you need the same retry semantics on `pagehide`.
59
-
60
- ## Install from npm
18
+ ### npm/yarn/pnpm/bun
61
19
 
62
20
  ```bash
63
21
  npm install @behindthescenes/analytics
@@ -75,174 +33,458 @@ yarn add @behindthescenes/analytics
75
33
  bun add @behindthescenes/analytics
76
34
  ```
77
35
 
78
- ```ts
79
- import { createBTSAnalytics } from "@behindthescenes/analytics";
36
+ ### Browser Bundle (Hosted)
80
37
 
81
- const analytics = createBTSAnalytics({
82
- siteKey: "your-public-site-key",
83
- });
38
+ ```html
39
+ <script type="module">
40
+ import { createBTSAnalytics } from "https://behindthescenes.com/sdk/analytics/latest/browser/browser.js";
84
41
 
85
- // Optional explicit-only examples:
86
- // analytics.identify("user_123", { email: "fan@example.com" });
87
- // analytics.track("video_played", { videoId: "intro-01" });
88
- // analytics.trackStandard("lead", { formId: "newsletter" });
89
- // analytics.trackStandard("begin_checkout", { checkoutId: "offer_123" });
90
- // const { journeyToken } = await analytics.startFunnel("/landing");
91
- // const checkoutUrl = analytics.decorateUrl("https://behindthescenes.com/checkout", journeyToken);
92
- // await analytics.flushNow();
42
+ window.btsAnalytics = createBTSAnalytics({
43
+ siteKey: "your-public-site-key"
44
+ });
45
+ </script>
93
46
  ```
94
47
 
95
- ## Endpoint Override
48
+ ## Quick Start
96
49
 
97
- The SDK defaults to the production BTS analytics endpoint. Override `endpoint` only for staging, local development, or a custom proxy.
50
+ ```typescript
51
+ import { createBTSAnalytics } from "@behindthescenes/analytics";
98
52
 
99
- ```ts
100
53
  const analytics = createBTSAnalytics({
101
54
  siteKey: "your-public-site-key",
102
- endpoint: "https://staging-api.bts.dev/v2/website/analytics",
103
55
  });
104
56
  ```
105
57
 
106
- ## Contact forms
58
+ ## Setup in BTS
107
59
 
108
- Use `submitContactForm(...)` when an external website needs to submit a simple contact enquiry into the space's GoHighLevel contact list.
60
+ Before using the SDK, configure your site in the BTS platform:
109
61
 
110
- ```ts
111
- await analytics.submitContactForm({
112
- locationId: "your-ghl-location-id",
113
- email: "fan@example.com",
114
- subject: "Partnership enquiry",
115
- body: "Tell us more about working together.",
116
- });
117
- ```
62
+ ### 1. Get Your Site Key
118
63
 
119
- Required fields:
64
+ - In BTS, navigate to your creator dashboard
65
+ - Go to **Settings > Website Integration**
66
+ - Copy your **Public Site Key**
120
67
 
121
- - `locationId`: the GoHighLevel location configured for the BTS space
122
- - `email`: the visitor's email address
123
- - `subject`: short enquiry subject
124
- - `body`: the message or "tell us more" text
68
+ ### 2. Configure GoHighLevel (Optional)
125
69
 
126
- Optional fields:
70
+ For contact form submissions:
127
71
 
128
- - `name`, `firstName`, `lastName`: contact identity fields
129
- - `source`: defaults to `behind_the_scenes` when omitted
130
- - `tags`: extra GHL tags to attach to the contact
131
- - `customFields`: GHL custom fields to pass through directly
132
- - `metadata`: extra BTS metadata; values are forwarded as `bts_meta_<key>` contact fields
72
+ - Go to **Settings > Integrations > GoHighLevel**
73
+ - Connect your GHL account
74
+ - Note your **Location ID** for use with `submitContactForm()`
133
75
 
134
- `subject` and `body` are also stored as BTS metadata fields on the GHL contact (`bts_meta_subject` and `bts_meta_body`). This helper creates or updates the contact only; it does not create a GHL opportunity.
76
+ ### 3. Enable External Site Tracking
135
77
 
136
- The SDK uses the production website analytics endpoint by default:
78
+ Ensure your domain is added to the allowed domains list in your BTS space settings.
137
79
 
138
- ```ts
139
- const analytics = createBTSAnalytics({
80
+ ## Framework Setup
81
+
82
+ ### React
83
+
84
+ ```tsx
85
+ // BTSAnalyticsProvider.tsx
86
+ import { createBTSAnalytics, BTSAnalytics } from "@behindthescenes/analytics";
87
+ import { createContext, PropsWithChildren, useContext, useMemo } from "react";
88
+
89
+ const BTSAnalyticsContext = createContext<BTSAnalytics | null>(null);
90
+
91
+ export function BTSAnalyticsProvider({ children }: PropsWithChildren) {
92
+ const analytics = useMemo(() =>
93
+ createBTSAnalytics({
94
+ siteKey: "your-public-site-key",
95
+ autoPageviews: true,
96
+ }),
97
+ []
98
+ );
99
+
100
+ return (
101
+ <BTSAnalyticsContext.Provider value={analytics}>
102
+ {children}
103
+ </BTSAnalyticsContext.Provider>
104
+ );
105
+ }
106
+
107
+ export function useBTSAnalytics() {
108
+ const analytics = useContext(BTSAnalyticsContext);
109
+ if (!analytics) throw new Error("useBTSAnalytics must be used inside BTSAnalyticsProvider");
110
+ return analytics;
111
+ }
112
+ ```
113
+
114
+ Usage:
115
+ ```tsx
116
+ const analytics = useBTSAnalytics();
117
+ analytics.track("cta_clicked", { placement: "hero" });
118
+ ```
119
+
120
+ ### Next.js
121
+
122
+ ```tsx
123
+ // app/providers.tsx
124
+ "use client";
125
+
126
+ import { createBTSAnalytics, BTSAnalytics } from "@behindthescenes/analytics";
127
+ import { createContext, PropsWithChildren, useContext, useMemo } from "react";
128
+
129
+ const BTSAnalyticsContext = createContext<BTSAnalytics | null>(null);
130
+
131
+ export function Providers({ children }: PropsWithChildren) {
132
+ const analytics = useMemo(() =>
133
+ createBTSAnalytics({
134
+ siteKey: "your-public-site-key",
135
+ autoPageviews: true,
136
+ }),
137
+ []
138
+ );
139
+
140
+ return (
141
+ <BTSAnalyticsContext.Provider value={analytics}>
142
+ {children}
143
+ </BTSAnalyticsContext.Provider>
144
+ );
145
+ }
146
+
147
+ export function useBTSAnalytics() {
148
+ const analytics = useContext(BTSAnalyticsContext);
149
+ if (!analytics) throw new Error("useBTSAnalytics must be used inside Providers");
150
+ return analytics;
151
+ }
152
+ ```
153
+
154
+ ### Remix
155
+
156
+ ```tsx
157
+ // app/root.tsx
158
+ import { createBTSAnalytics } from "@behindthescenes/analytics";
159
+ import { useEffect, useMemo } from "react";
160
+ import { Outlet, useLocation } from "react-router";
161
+
162
+ export const analytics = createBTSAnalytics({
140
163
  siteKey: "your-public-site-key",
164
+ autoPageviews: false,
141
165
  });
142
- ```
143
166
 
144
- `submitContactForm(...)` derives the sibling contact endpoint from the configured analytics URL and posts to `/v2/website/ghl/leads`.
167
+ export function Layout() {
168
+ const location = useLocation();
169
+ const path = useMemo(() => location.pathname + location.search, [location]);
145
170
 
146
- Example form wiring:
171
+ useEffect(() => {
172
+ analytics.page(path);
173
+ }, [path]);
147
174
 
148
- ```ts
149
- const form = document.querySelector<HTMLFormElement>("#contact-form");
175
+ return <Outlet />;
176
+ }
177
+ ```
150
178
 
151
- form?.addEventListener("submit", async (event) => {
152
- event.preventDefault();
179
+ ### Vue
153
180
 
154
- const data = new FormData(form);
155
- await analytics.submitContactForm({
156
- locationId: "your-ghl-location-id",
157
- email: String(data.get("email") ?? ""),
158
- subject: String(data.get("subject") ?? ""),
159
- body: String(data.get("body") ?? ""),
160
- name: String(data.get("name") ?? ""),
161
- metadata: {
162
- page: window.location.pathname,
163
- },
164
- });
181
+ ```typescript
182
+ // src/plugins/bts-analytics.ts
183
+ import { createBTSAnalytics } from "@behindthescenes/analytics";
184
+ import type { App } from "vue";
185
+
186
+ export const analytics = createBTSAnalytics({
187
+ siteKey: "your-public-site-key",
188
+ autoPageviews: true,
165
189
  });
190
+
191
+ export function installBTSAnalytics(app: App) {
192
+ app.provide("btsAnalytics", analytics);
193
+ }
166
194
  ```
167
195
 
168
- ## BTS-hosted browser bundle
196
+ ```vue
197
+ <script setup lang="ts">
198
+ import { inject } from "vue";
199
+ import type { BTSAnalytics } from "@behindthescenes/analytics";
169
200
 
170
- ```html
171
- <script type="module">
172
- import { createBTSAnalytics } from "https://app.behindthescenes.com/sdk/analytics/latest/browser/browser.js";
201
+ const analytics = inject<BTSAnalytics>("btsAnalytics")!;
173
202
 
174
- window.btsAnalytics = createBTSAnalytics({
175
- siteKey: "your-public-site-key"
176
- });
203
+ function trackEvent() {
204
+ analytics.track("button_clicked", { buttonId: "cta" });
205
+ }
177
206
  </script>
178
207
  ```
179
208
 
180
- The hosted bundle is generated from `packages/analytics/dist` and synced into `apps/bts-web/public/sdk/analytics`.
209
+ ### Plain JavaScript
210
+
211
+ ```html
212
+ <script async src="https://api.bts.it.com/v2/website/analytics/sdk/analytics/latest/browser/browser.js"></script>
213
+ <script>
214
+ window.btsDataLayer = window.btsDataLayer || [];
215
+ function bts(){window.btsDataLayer.push(arguments);}
216
+ bts("js", new Date());
217
+ bts("config", "your-public-site-key", {
218
+ autoPageviews: true
219
+ });
220
+ </script>
221
+ ```
181
222
 
182
- Release builds do not emit or publish source maps. The package build fails if any `.map` files are present in `dist`.
223
+ **See the [docs folder](./docs) for complete framework guides:**
224
+ - [Frameworks Guide](./docs/frameworks.md) - React, Next.js, Remix, Vue, Svelte, Angular, SolidJS
225
+ - [Advanced Setup](./docs/advanced-setup.md) - Custom endpoints, request signing, GA integration, SPA config
226
+ - [API Reference](./docs/api-reference.md) - Complete API documentation
183
227
 
184
- ## Releases
228
+ ## Auto-Tracked Events
185
229
 
186
- Publishing the npm package bumps the version with standard semver release types:
230
+ The SDK automatically captures these events without additional code:
187
231
 
188
- - `patch`: bug fixes and backwards-compatible corrections, for example `1.2.3` to `1.2.4`
189
- - `minor`: backwards-compatible features, for example `1.2.3` to `1.3.0`
190
- - `major`: breaking changes, for example `1.2.3` to `2.0.0`
232
+ | Event | Trigger | Description |
233
+ |-------|---------|-------------|
234
+ | `page_view` | Page load, SPA navigation | Initial load and history changes |
235
+ | `outbound_click` | External link clicks | Links to different origins |
236
+ | `button_click` | Button interactions | `button`, `[role="button"]`, or `[data-bts-track-click]` |
237
+ | `form_submit` | Form submissions | Any form submit event |
238
+ | `search` | GET search forms | Forms with query params `q`, `query`, or `search` |
239
+ | `view_content` | Element visibility | Elements with `data-bts-view-content` |
191
240
 
192
- Run the GitHub Actions release workflow manually and choose the release type to publish the next package version. The workflow reads the latest published npm version, increments it, validates the package, publishes it, and commits the updated `package.json` version.
241
+ ### Content View Tracking
193
242
 
194
- For local publishing, set the release type before running the publish script:
243
+ Opt-in to automatic content-view tracking by marking elements:
195
244
 
196
- ```sh
197
- RELEASE_TYPE=minor bun run release:publish
245
+ ```html
246
+ <article
247
+ data-bts-view-content="offer_123"
248
+ data-bts-content-type="offer"
249
+ data-bts-content-title="Creator Accelerator"
250
+ >
251
+ Creator Accelerator
252
+ </article>
198
253
  ```
199
254
 
200
- ## Standard events
255
+ ## Standard Events
201
256
 
202
- ```ts
203
- analytics.trackStandard("view_content", {
204
- contentId: "offer-42",
205
- });
257
+ Use `trackStandard()` for built-in conversion and engagement events:
206
258
 
259
+ ```typescript
260
+ // Engagement
261
+ analytics.trackStandard("page_view");
262
+ analytics.trackStandard("view_content", { contentId: "offer-42" });
263
+ analytics.trackStandard("search", { query: "pricing" });
264
+
265
+ // Lead & Account
266
+ analytics.trackStandard("lead", { formId: "newsletter" });
267
+ analytics.trackStandard("sign_up", { method: "email" });
268
+
269
+ // E-commerce
270
+ analytics.trackStandard("begin_checkout", { checkoutId: "offer_123" });
271
+ analytics.trackStandard("add_payment_info", { checkoutId: "offer_123" });
207
272
  analytics.trackStandard("purchase", {
208
- currency: "USD",
209
- monetaryValue: 149,
210
273
  orderId: "order_123",
274
+ monetaryValue: 149,
275
+ currency: "USD"
211
276
  });
277
+
278
+ // Engagement
279
+ analytics.trackStandard("outbound_click", { url: "https://partner.com" });
280
+ analytics.trackStandard("button_click", { buttonId: "cta-primary" });
281
+ analytics.trackStandard("form_submit", { formId: "contact" });
212
282
  ```
213
283
 
214
- Supported standard events:
284
+ ### Legacy Event Aliases
215
285
 
216
- - `page_view`
217
- - `view_content`
218
- - `search`
219
- - `lead`
220
- - `sign_up`
221
- - `begin_checkout`
222
- - `add_payment_info`
223
- - `purchase`
224
- - `outbound_click`
225
- - `button_click`
226
- - `form_submit`
286
+ These aliases are automatically normalized to standard events:
227
287
 
228
- Legacy aliases such as `lead_capture`, `registration_complete`, `checkout_started`, and `purchase_completed` are normalized into the standard conversion catalog.
288
+ | Legacy Name | Standard Event |
289
+ |-------------|----------------|
290
+ | `$pageview` | `page_view` |
291
+ | `checkout_started` | `begin_checkout` |
292
+ | `lead_capture` | `lead` |
293
+ | `purchase_completed` | `purchase` |
294
+ | `registration_complete` | `sign_up` |
229
295
 
230
- ## Custom events
296
+ ## Custom Events
297
+
298
+ Track arbitrary events with custom properties:
299
+
300
+ ```typescript
301
+ analytics.track("video_played", {
302
+ videoId: "intro-01",
303
+ duration: 120,
304
+ autoplay: false
305
+ });
231
306
 
232
- ```ts
233
307
  analytics.track("cta_clicked", {
234
308
  ctaId: "hero-primary",
235
309
  placement: "hero",
310
+ variant: "blue"
311
+ });
312
+ ```
313
+
314
+ ## User Identification
315
+
316
+ Associate events with a known user:
317
+
318
+ ```typescript
319
+ analytics.identify("user_123", {
320
+ email: "fan@example.com",
321
+ name: "John Doe",
322
+ plan: "pro"
236
323
  });
237
324
  ```
238
325
 
239
- ## Request signing headers
326
+ ## Contact Form Submission
327
+
328
+ Submit contact enquiries directly to GoHighLevel:
329
+
330
+ ```typescript
331
+ await analytics.submitContactForm({
332
+ locationId: "your-ghl-location-id",
333
+ email: "fan@example.com",
334
+ subject: "Partnership enquiry",
335
+ body: "Tell us more about working together.",
336
+ name: "Jane Smith",
337
+ source: "website_contact",
338
+ tags: ["partnership", "high-value"],
339
+ customFields: {
340
+ company: "Acme Inc"
341
+ },
342
+ metadata: {
343
+ page: window.location.pathname,
344
+ referrer: document.referrer
345
+ }
346
+ });
347
+ ```
348
+
349
+ ### Required Fields
350
+
351
+ - `locationId`: Your GoHighLevel location ID
352
+ - At least one of `email` or `phone`
353
+
354
+ ### Optional Fields
355
+
356
+ - `subject`, `body`: Message content (stored as `bts_meta_subject` and `bts_meta_body`)
357
+ - `name`, `firstName`, `lastName`: Contact identity
358
+ - `source`: Defaults to `behind_the_scenes`
359
+ - `tags`: Array of GHL tags to attach
360
+ - `customFields`: Direct GHL custom field mappings
361
+ - `metadata`: Additional context forwarded as `bts_meta_<key>` fields
240
362
 
241
- Use `requestHeaders` to attach custom anti-spoof headers to every API request. The hook receives the serialized request body, so you can sign the exact payload being sent.
363
+ ## Journey Tracking (Cross-Site Attribution)
242
364
 
243
- ```ts
365
+ Track visitors across external sites and BTS properties:
366
+
367
+ ```typescript
368
+ // Start a funnel journey
369
+ const { journeyToken, journeyId, expiresAt } = await analytics.startFunnel("/landing");
370
+
371
+ // Decorate BTS URLs with journey context
372
+ const checkoutUrl = analytics.decorateUrl(
373
+ "https://behindthescenes.com/checkout",
374
+ journeyToken
375
+ );
376
+
377
+ // Get the persisted token for later use
378
+ const token = analytics.getPersistedJourneyToken();
379
+
380
+ // Notify handoff after visitor reaches BTS
381
+ await analytics.notifyHandoff(journeyToken, {
382
+ source: "external_landing_page",
383
+ campaign: "summer_2024"
384
+ });
385
+ ```
386
+
387
+ ## Configuration Options
388
+
389
+ ```typescript
244
390
  const analytics = createBTSAnalytics({
245
- siteKey: "your-public-site-key",
391
+ siteKey: "required-site-key",
392
+
393
+ // Auto-tracking configuration
394
+ autoTrack: {
395
+ pageviews: true, // Initial page_view + SPA navigation
396
+ history: true, // pushState/replaceState hooks
397
+ outboundLinks: true, // External link clicks
398
+ buttonClicks: true, // Button interactions
399
+ formSubmissions: true, // Form submit events
400
+ search: true, // GET search forms
401
+ viewContent: true // IntersectionObserver content views
402
+ },
403
+
404
+ // Or disable all auto-tracking
405
+ autoTrack: false,
406
+
407
+ // Legacy option: disable pageviews (also disables history hooks)
408
+ autoPageviews: false,
409
+
410
+ // Debug mode (logs to console)
411
+ debug: false,
412
+
413
+ // Flush interval in milliseconds (default: 300ms)
414
+ flushIntervalMs: 300,
415
+
416
+ // Google Analytics integration
417
+ googleAnalytics: {
418
+ loadTag: true,
419
+ measurementId: "G-XXXXXXXXXX"
420
+ },
421
+
422
+ // Custom request signing
423
+ requestHeaders: async ({ bodyText, path, headers }) => {
424
+ return {
425
+ ...headers,
426
+ "X-Signature": await signPayload(bodyText)
427
+ };
428
+ }
429
+ });
430
+ ```
431
+
432
+ ### Auto-Tracking Behavior
433
+
434
+ - `autoTrack: false` - Disables all automatic tracking
435
+ - `autoPageviews: false` - Disables initial pageview and SPA navigation
436
+ - Setting `pageviews: false` in `autoTrack` object also disables `history` hooks
437
+ - Disabling pageviews prevents duplicate tracking when implementing custom pageview logic
438
+
439
+ ## Google Analytics Integration
440
+
441
+ ### Detection Mode (Default)
442
+
443
+ The SDK automatically detects existing GA installations and includes context:
444
+
445
+ - `ga_client_id`: From `_ga` cookie
446
+ - `ga_tag_installed`: Boolean if gtag detected
447
+ - `ga_measurement_id`: From script tag
448
+
449
+ ### Proxy Mode
450
+
451
+ Load GA tag for scanner visibility while forwarding events server-side:
452
+
453
+ ```typescript
454
+ const analytics = createBTSAnalytics({
455
+ siteKey: "your-site-key",
456
+ googleAnalytics: {
457
+ loadTag: true,
458
+ measurementId: "G-XXXXXXXXXX"
459
+ }
460
+ });
461
+ ```
462
+
463
+ This loads the Google tag with `send_page_view: false` so BTS can forward events while GA scanners detect an installed tag.
464
+
465
+ ## Attribution & UTM Parameters
466
+
467
+ The SDK automatically captures and persists:
468
+
469
+ **UTM Parameters:**
470
+ - `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`
471
+
472
+ **Click IDs:**
473
+ - `fbclid`, `gclid`, `gbraid`, `wbraid`
474
+ - `li_fat_id`, `msclkid`, `ttclid`
475
+
476
+ **Cookies:**
477
+ - `_fbp`, `_fbc` (Facebook Pixel)
478
+
479
+ Attribution data is stored in localStorage and attached to every event, preserving the original landing URL and referrer across the session.
480
+
481
+ ## Request Signing
482
+
483
+ Add custom headers for request validation:
484
+
485
+ ```typescript
486
+ const analytics = createBTSAnalytics({
487
+ siteKey: "your-site-key",
246
488
  requestHeaders: async ({ bodyText, headers, path }) => {
247
489
  const timestamp = new Date().toISOString();
248
490
  const signature = await signAnalyticsPayload(`${timestamp}:${path}:${bodyText}`);
@@ -256,12 +498,88 @@ const analytics = createBTSAnalytics({
256
498
  });
257
499
  ```
258
500
 
259
- When `requestHeaders` is configured, page-unload flushes use `fetch(..., { keepalive: true })` instead of `sendBeacon`, because browsers do not allow custom headers on beacon requests.
501
+ When `requestHeaders` is configured, the SDK uses `fetch(..., { keepalive: true })` instead of `sendBeacon` for page-unload events, enabling the same retry semantics for all requests.
502
+
503
+ ## Flush Reliability
504
+
505
+ The SDK queues events and flushes them in batches:
260
506
 
261
- ## Journeys
507
+ - Automatic flush every 300ms (configurable)
508
+ - Immediate flush when queue reaches 50 events
509
+ - Failed batches are requeued and retried
510
+ - Page unload uses `sendBeacon` (or `fetch` with `keepalive` when custom headers are set)
262
511
 
263
- ```ts
264
- const { journeyToken } = await analytics.startFunnel();
265
- const checkoutUrl = analytics.decorateUrl("https://behindthescenes.com/checkout", journeyToken);
512
+ ### Manual Flush
513
+
514
+ ```typescript
515
+ // Force immediate flush
516
+ await analytics.flushNow();
266
517
  ```
267
518
 
519
+ ## Cleanup
520
+
521
+ Remove event listeners and flush pending events:
522
+
523
+ ```typescript
524
+ analytics.destroy();
525
+ ```
526
+
527
+ This:
528
+ - Removes click, submit, and navigation listeners
529
+ - Disconnects IntersectionObserver and MutationObserver
530
+ - Flushes any pending events
531
+ - Prevents further event tracking
532
+
533
+ ## API Reference
534
+
535
+ ### Methods
536
+
537
+ | Method | Description |
538
+ |--------|-------------|
539
+ | `track(eventName, properties?)` | Track a custom event |
540
+ | `trackStandard(eventName, properties?)` | Track a standard event |
541
+ | `identify(userId, traits?)` | Identify the current user |
542
+ | `page(path?)` | Manually trigger a page view |
543
+ | `startFunnel(entryPath?)` | Start a cross-site journey |
544
+ | `decorateUrl(url, journeyToken?)` | Append site key and journey to URL |
545
+ | `getPersistedJourneyToken()` | Get stored journey token |
546
+ | `notifyHandoff(journeyToken, context?)` | Notify BTS of journey handoff |
547
+ | `submitContactForm(input)` | Submit to GoHighLevel |
548
+ | `flushNow()` | Immediately flush event queue |
549
+ | `listStandardEvents()` | Get list of standard event names |
550
+ | `destroy()` | Cleanup and remove listeners |
551
+
552
+ ### Types
553
+
554
+ ```typescript
555
+ type BTSAnalyticsStandardEventName =
556
+ | "page_view"
557
+ | "view_content"
558
+ | "search"
559
+ | "lead"
560
+ | "sign_up"
561
+ | "begin_checkout"
562
+ | "add_payment_info"
563
+ | "purchase"
564
+ | "outbound_click"
565
+ | "button_click"
566
+ | "form_submit";
567
+
568
+ type BTSAnalyticsEventType = "page_view" | "custom" | "identify" | "conversion";
569
+ ```
570
+
571
+ ## Releases
572
+
573
+ Publishing the npm package bumps the version with standard semver release types:
574
+
575
+ - `patch`: Bug fixes and backwards-compatible corrections (e.g., `1.2.3` to `1.2.4`)
576
+ - `minor`: Backwards-compatible features (e.g., `1.2.3` to `1.3.0`)
577
+ - `major`: Breaking changes (e.g., `1.2.3` to `2.0.0`)
578
+
579
+ Run the GitHub Actions release workflow manually and choose the release type to publish the next package version. The workflow reads the latest published npm version, increments it, validates the package, publishes it, and commits the updated `package.json` version.
580
+
581
+ For local publishing, set the release type before running the publish script:
582
+
583
+ ```sh
584
+ RELEASE_TYPE=minor bun run release:publish
585
+ ```