@bagdock/analytics
Advanced tools
+11
-1
@@ -30,2 +30,10 @@ type EventType = 'click' | 'lead' | 'sale' | 'signup' | 'embed_render' | 'share' | 'qr_scan' | 'deep_link_open' | 'page_view' | 'reward_redeemed' | 'points_earned' | 'referral_completed'; | ||
| } | ||
| interface UTMParams { | ||
| utm_source?: string; | ||
| utm_medium?: string; | ||
| utm_campaign?: string; | ||
| utm_term?: string; | ||
| utm_content?: string; | ||
| } | ||
| declare function parseUTM(url?: string): UTMParams; | ||
| declare class BagdockAnalytics { | ||
@@ -37,3 +45,5 @@ private config; | ||
| private flushing; | ||
| private utm; | ||
| constructor(config: BagdockAnalyticsConfig); | ||
| getUTM(): UTMParams; | ||
| track(event: TrackableEvent): void; | ||
@@ -64,2 +74,2 @@ trackClick(linkId: string, referralCode?: string): void; | ||
| export { BagdockAnalytics, type BagdockAnalyticsConfig, type EventType, type TrackableEvent }; | ||
| export { BagdockAnalytics, type BagdockAnalyticsConfig, type EventType, type TrackableEvent, type UTMParams, parseUTM }; |
+11
-1
@@ -30,2 +30,10 @@ type EventType = 'click' | 'lead' | 'sale' | 'signup' | 'embed_render' | 'share' | 'qr_scan' | 'deep_link_open' | 'page_view' | 'reward_redeemed' | 'points_earned' | 'referral_completed'; | ||
| } | ||
| interface UTMParams { | ||
| utm_source?: string; | ||
| utm_medium?: string; | ||
| utm_campaign?: string; | ||
| utm_term?: string; | ||
| utm_content?: string; | ||
| } | ||
| declare function parseUTM(url?: string): UTMParams; | ||
| declare class BagdockAnalytics { | ||
@@ -37,3 +45,5 @@ private config; | ||
| private flushing; | ||
| private utm; | ||
| constructor(config: BagdockAnalyticsConfig); | ||
| getUTM(): UTMParams; | ||
| track(event: TrackableEvent): void; | ||
@@ -64,2 +74,2 @@ trackClick(linkId: string, referralCode?: string): void; | ||
| export { BagdockAnalytics, type BagdockAnalyticsConfig, type EventType, type TrackableEvent }; | ||
| export { BagdockAnalytics, type BagdockAnalyticsConfig, type EventType, type TrackableEvent, type UTMParams, parseUTM }; |
+50
-3
@@ -23,5 +23,37 @@ "use strict"; | ||
| __export(index_exports, { | ||
| BagdockAnalytics: () => BagdockAnalytics | ||
| BagdockAnalytics: () => BagdockAnalytics, | ||
| parseUTM: () => parseUTM | ||
| }); | ||
| module.exports = __toCommonJS(index_exports); | ||
| function parseUTM(url) { | ||
| if (typeof window === "undefined" && !url) return {}; | ||
| try { | ||
| const params = new URL(url || window.location.href).searchParams; | ||
| const utm = {}; | ||
| for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]) { | ||
| const val = params.get(key); | ||
| if (val) utm[key] = val; | ||
| } | ||
| return utm; | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| var UTM_STORAGE_KEY = "bagdock_utm"; | ||
| function persistUTM(utm) { | ||
| if (typeof sessionStorage === "undefined") return; | ||
| try { | ||
| const existing = JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| sessionStorage.setItem(UTM_STORAGE_KEY, JSON.stringify({ ...existing, ...utm })); | ||
| } catch { | ||
| } | ||
| } | ||
| function getPersistedUTM() { | ||
| if (typeof sessionStorage === "undefined") return {}; | ||
| try { | ||
| return JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| var DEFAULT_BASE_URL = "https://loyalty-api.bagdock.com"; | ||
@@ -37,2 +69,3 @@ var DEFAULT_FLUSH_INTERVAL = 5e3; | ||
| flushing = false; | ||
| utm = {}; | ||
| constructor(config) { | ||
@@ -50,2 +83,9 @@ this.config = { | ||
| if (typeof window !== "undefined") { | ||
| const freshUTM = parseUTM(); | ||
| if (Object.keys(freshUTM).length > 0) { | ||
| persistUTM(freshUTM); | ||
| this.utm = freshUTM; | ||
| } else { | ||
| this.utm = getPersistedUTM(); | ||
| } | ||
| window.addEventListener("beforeunload", () => this.flush()); | ||
@@ -60,2 +100,5 @@ if (typeof document !== "undefined") { | ||
| } | ||
| getUTM() { | ||
| return { ...this.utm }; | ||
| } | ||
| track(event) { | ||
@@ -66,6 +109,9 @@ if (this.isDuplicate(event)) { | ||
| } | ||
| const hasUTM = Object.keys(this.utm).length > 0; | ||
| const metadata = hasUTM ? { ...this.utm, ...event.metadata } : event.metadata; | ||
| this.queue.push({ | ||
| ...event, | ||
| landingPage: event.landingPage || (typeof window !== "undefined" ? window.location.href : void 0), | ||
| referrer: event.referrer || (typeof document !== "undefined" ? document.referrer : void 0) | ||
| referrer: event.referrer || (typeof document !== "undefined" ? document.referrer : void 0), | ||
| metadata | ||
| }); | ||
@@ -169,3 +215,4 @@ this.log("Queued:", event.eventType, `(${this.queue.length}/${this.config.batchSize})`); | ||
| 0 && (module.exports = { | ||
| BagdockAnalytics | ||
| BagdockAnalytics, | ||
| parseUTM | ||
| }); |
+48
-2
| // src/index.ts | ||
| function parseUTM(url) { | ||
| if (typeof window === "undefined" && !url) return {}; | ||
| try { | ||
| const params = new URL(url || window.location.href).searchParams; | ||
| const utm = {}; | ||
| for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]) { | ||
| const val = params.get(key); | ||
| if (val) utm[key] = val; | ||
| } | ||
| return utm; | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| var UTM_STORAGE_KEY = "bagdock_utm"; | ||
| function persistUTM(utm) { | ||
| if (typeof sessionStorage === "undefined") return; | ||
| try { | ||
| const existing = JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| sessionStorage.setItem(UTM_STORAGE_KEY, JSON.stringify({ ...existing, ...utm })); | ||
| } catch { | ||
| } | ||
| } | ||
| function getPersistedUTM() { | ||
| if (typeof sessionStorage === "undefined") return {}; | ||
| try { | ||
| return JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| var DEFAULT_BASE_URL = "https://loyalty-api.bagdock.com"; | ||
@@ -12,2 +43,3 @@ var DEFAULT_FLUSH_INTERVAL = 5e3; | ||
| flushing = false; | ||
| utm = {}; | ||
| constructor(config) { | ||
@@ -25,2 +57,9 @@ this.config = { | ||
| if (typeof window !== "undefined") { | ||
| const freshUTM = parseUTM(); | ||
| if (Object.keys(freshUTM).length > 0) { | ||
| persistUTM(freshUTM); | ||
| this.utm = freshUTM; | ||
| } else { | ||
| this.utm = getPersistedUTM(); | ||
| } | ||
| window.addEventListener("beforeunload", () => this.flush()); | ||
@@ -35,2 +74,5 @@ if (typeof document !== "undefined") { | ||
| } | ||
| getUTM() { | ||
| return { ...this.utm }; | ||
| } | ||
| track(event) { | ||
@@ -41,6 +83,9 @@ if (this.isDuplicate(event)) { | ||
| } | ||
| const hasUTM = Object.keys(this.utm).length > 0; | ||
| const metadata = hasUTM ? { ...this.utm, ...event.metadata } : event.metadata; | ||
| this.queue.push({ | ||
| ...event, | ||
| landingPage: event.landingPage || (typeof window !== "undefined" ? window.location.href : void 0), | ||
| referrer: event.referrer || (typeof document !== "undefined" ? document.referrer : void 0) | ||
| referrer: event.referrer || (typeof document !== "undefined" ? document.referrer : void 0), | ||
| metadata | ||
| }); | ||
@@ -143,3 +188,4 @@ this.log("Queued:", event.eventType, `(${this.queue.length}/${this.config.batchSize})`); | ||
| export { | ||
| BagdockAnalytics | ||
| BagdockAnalytics, | ||
| parseUTM | ||
| }; |
+1
-1
| { | ||
| "name": "@bagdock/analytics", | ||
| "version": "0.1.5", | ||
| "version": "0.2.0", | ||
| "description": "Bagdock Analytics SDK — lightweight client-side event tracking with batching and dedup", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
+231
-34
@@ -7,3 +7,3 @@ ``` | ||
| ---+++---++ ++++---++---+++---++---+++---++---+++---++---++---++------++++ | ||
| ----++ ---++--------++---++----++---++ ---++---++ ---+---++ -------++ | ||
| ----++ ---++--------++---++----++---+++---++---++ ---+---++ -------++ | ||
| ----+----+---+++---++---++----++---++----++---++---+++--++ --------+---++ | ||
@@ -18,5 +18,6 @@ ---------++--------+++--------+++--------++ -------+++ -------++---++----++ | ||
| The official Bagdock Analytics SDK — lightweight client-side event tracking with automatic batching and deduplication. | ||
| Track events, attribute conversions, and measure engagement across Bagdock-powered self-storage apps. The SDK batches events client-side, deduplicates within a configurable window, captures UTM attribution automatically, and flushes gracefully on page unload. | ||
| [](https://www.npmjs.com/package/@bagdock/analytics) | ||
| [](https://bundlephobia.com/package/@bagdock/analytics) | ||
| [](LICENSE) | ||
@@ -28,18 +29,33 @@ | ||
| npm install @bagdock/analytics | ||
| # or | ||
| bun add @bagdock/analytics | ||
| ``` | ||
| ```bash | ||
| yarn add @bagdock/analytics | ||
| ``` | ||
| ## How tracking works | ||
| ```bash | ||
| pnpm add @bagdock/analytics | ||
| ``` | ||
| Your app calls `track()` methods. The SDK deduplicates, queues, and flushes events in batches to the Bagdock API over HTTPS. | ||
| ```bash | ||
| bun add @bagdock/analytics | ||
| ```mermaid | ||
| sequenceDiagram | ||
| participant App | ||
| participant SDK as @bagdock/analytics | ||
| participant API as Bagdock API | ||
| App->>SDK: track({ eventType: 'lead' }) | ||
| SDK->>SDK: Dedup check | ||
| SDK->>SDK: Attach UTM metadata | ||
| SDK->>SDK: Queue event | ||
| Note over SDK: Flush on timer (5 s) or batch full (25) | ||
| SDK->>API: POST /api/loyalty/events | ||
| API-->>SDK: 200 OK | ||
| ``` | ||
| ## Quick start | ||
| On `beforeunload` and `visibilitychange`, the SDK flushes with `navigator.sendBeacon` so no events are lost during navigation. | ||
| --- | ||
| ## Get started | ||
| ```typescript | ||
@@ -49,43 +65,224 @@ import { BagdockAnalytics } from '@bagdock/analytics' | ||
| const analytics = new BagdockAnalytics({ | ||
| writeKey: 'ak_live_...', | ||
| apiKey: 'YOUR_API_KEY', | ||
| autoPageView: true, | ||
| }) | ||
| // Track a custom event | ||
| analytics.track('unit_viewed', { | ||
| unitId: 'unit_abc123', | ||
| unitSize: '10x10', | ||
| analytics.trackLead({ | ||
| operatorId: 'opreg_acme', | ||
| referralCode: 'REF123', | ||
| metadata: { source: 'pricing_page' }, | ||
| }) | ||
| // Track a page view | ||
| analytics.page('/facilities/downtown') | ||
| analytics.trackSale({ | ||
| operatorId: 'opreg_acme', | ||
| valuePence: 14900, | ||
| currency: 'GBP', | ||
| }) | ||
| // Flush immediately (e.g., before page unload) | ||
| await analytics.flush() | ||
| analytics.destroy() | ||
| ``` | ||
| ## Configuration | ||
| --- | ||
| | Option | Type | Default | Description | | ||
| |--------|------|---------|-------------| | ||
| | `writeKey` | `string` | — | **Required.** Your Bagdock analytics write key | | ||
| | `baseUrl` | `string` | `https://api.bagdock.com` | API base URL | | ||
| | `flushInterval` | `number` | `5000` | Flush interval in milliseconds | | ||
| | `maxBatchSize` | `number` | `25` | Max events per batch | | ||
| ## How to track UTM attribution | ||
| ## API | ||
| The SDK captures UTM parameters from the URL on init, persists them in `sessionStorage` for the tab's lifetime, and attaches them as metadata to every event. | ||
| ```typescript | ||
| // User lands on: https://example.com?utm_source=google&utm_medium=cpc&utm_campaign=spring | ||
| const analytics = new BagdockAnalytics({ apiKey: 'YOUR_API_KEY' }) | ||
| analytics.getUTM() | ||
| // → { utm_source: 'google', utm_medium: 'cpc', utm_campaign: 'spring' } | ||
| analytics.trackLead({ operatorId: 'opreg_acme' }) | ||
| // → event metadata includes utm_source, utm_medium, utm_campaign | ||
| ``` | ||
| Parse UTM parameters from any URL: | ||
| ```typescript | ||
| import { parseUTM } from '@bagdock/analytics' | ||
| parseUTM('https://example.com?utm_source=partner&utm_campaign=launch') | ||
| // → { utm_source: 'partner', utm_campaign: 'launch' } | ||
| ``` | ||
| ## How to use with Next.js App Router | ||
| Create a provider component that initializes the SDK once and tracks route changes: | ||
| ```tsx | ||
| 'use client' | ||
| import { useEffect, useRef } from 'react' | ||
| import { usePathname, useSearchParams } from 'next/navigation' | ||
| import { BagdockAnalytics } from '@bagdock/analytics' | ||
| export function AnalyticsProvider({ children }: { children: React.ReactNode }) { | ||
| const pathname = usePathname() | ||
| const searchParams = useSearchParams() | ||
| const ref = useRef<BagdockAnalytics | null>(null) | ||
| const prevPath = useRef('') | ||
| useEffect(() => { | ||
| if (!ref.current) { | ||
| ref.current = new BagdockAnalytics({ | ||
| apiKey: process.env.NEXT_PUBLIC_ANALYTICS_API_KEY!, | ||
| autoPageView: true, | ||
| }) | ||
| } | ||
| return () => { ref.current?.destroy(); ref.current = null } | ||
| }, []) | ||
| useEffect(() => { | ||
| const path = pathname + (searchParams?.toString() ? `?${searchParams}` : '') | ||
| if (path !== prevPath.current) { | ||
| prevPath.current = path | ||
| ref.current?.trackPageView() | ||
| } | ||
| }, [pathname, searchParams]) | ||
| return <>{children}</> | ||
| } | ||
| ``` | ||
| Wrap your root layout: | ||
| ```tsx | ||
| <AnalyticsProvider> | ||
| {children} | ||
| </AnalyticsProvider> | ||
| ``` | ||
| ## How to track embed and widget events | ||
| Track renders and clicks from embedded widgets on partner sites: | ||
| ```typescript | ||
| const analytics = new BagdockAnalytics({ apiKey: 'YOUR_API_KEY' }) | ||
| analytics.trackEmbedRender('opreg_acme') | ||
| analytics.trackClick('link_abc123', 'REF456') | ||
| ``` | ||
| ## How to track loyalty program events | ||
| Track points, rewards, and referrals: | ||
| ```typescript | ||
| analytics.track({ | ||
| eventType: 'points_earned', | ||
| memberId: 'mem_abc', | ||
| operatorId: 'opreg_acme', | ||
| valuePence: 500, | ||
| }) | ||
| analytics.track({ | ||
| eventType: 'reward_redeemed', | ||
| memberId: 'mem_abc', | ||
| metadata: { rewardId: 'rwd_xyz', tier: 'gold' }, | ||
| }) | ||
| analytics.track({ | ||
| eventType: 'referral_completed', | ||
| referralCode: 'REF123', | ||
| memberId: 'mem_abc', | ||
| }) | ||
| ``` | ||
| --- | ||
| ## Methods | ||
| | Method | Description | | ||
| |--------|-------------| | ||
| | `track(event, properties?)` | Track a custom event | | ||
| | `page(path?, properties?)` | Track a page view | | ||
| | `identify(userId, traits?)` | Identify a user | | ||
| | `track(event)` | Track any event with full control over the payload | | ||
| | `trackClick(linkId, referralCode?)` | Track a link click with optional referral attribution | | ||
| | `trackLead(params)` | Track a lead conversion | | ||
| | `trackSale(params)` | Track a completed sale | | ||
| | `trackPageView()` | Track a page view (captures URL and referrer automatically) | | ||
| | `trackEmbedRender(operatorId?)` | Track when an embedded widget renders | | ||
| | `getUTM()` | Return the current UTM attribution context | | ||
| | `flush()` | Flush the event queue immediately | | ||
| | `reset()` | Clear user identity and queue | | ||
| | `destroy()` | Flush remaining events, clear timers, remove listeners | | ||
| ## Zero dependencies | ||
| ## Types | ||
| This SDK has no external runtime dependencies. It uses the native `fetch` API and is designed to be as lightweight as possible for client-side use. | ||
| ### `TrackableEvent` | ||
| ```typescript | ||
| interface TrackableEvent { | ||
| eventType: EventType | ||
| linkId?: string | ||
| memberId?: string | ||
| operatorId?: string | ||
| referralCode?: string | ||
| valuePence?: number | ||
| currency?: string | ||
| landingPage?: string | ||
| referrer?: string | ||
| metadata?: Record<string, unknown> | ||
| } | ||
| ``` | ||
| ### `EventType` | ||
| ```typescript | ||
| type EventType = | ||
| | 'click' | 'lead' | 'sale' | 'signup' | 'embed_render' | ||
| | 'share' | 'qr_scan' | 'deep_link_open' | 'page_view' | ||
| | 'reward_redeemed' | 'points_earned' | 'referral_completed' | ||
| ``` | ||
| ### Exports | ||
| | Export | Type | Description | | ||
| |--------|------|-------------| | ||
| | `BagdockAnalytics` | class | Main SDK class | | ||
| | `parseUTM` | function | Parse UTM parameters from a URL | | ||
| | `BagdockAnalyticsConfig` | interface | Constructor config shape | | ||
| | `TrackableEvent` | interface | Event payload shape | | ||
| | `EventType` | type | Union of supported event type strings | | ||
| | `UTMParams` | interface | UTM parameter shape | | ||
| ## How to configure the SDK | ||
| | Option | Type | Default | Description | | ||
| |--------|------|---------|-------------| | ||
| | `apiKey` | `string` | — | **Required.** Your Bagdock API key | | ||
| | `baseUrl` | `string` | `https://loyalty-api.bagdock.com` | API base URL | | ||
| | `flushIntervalMs` | `number` | `5000` | How often to flush queued events (ms) | | ||
| | `batchSize` | `number` | `25` | Max events per flush batch | | ||
| | `dedupWindowMs` | `number` | `500` | Window for dropping duplicate events (ms) | | ||
| | `autoPageView` | `boolean` | `false` | Track a page view on init | | ||
| | `debug` | `boolean` | `false` | Log SDK activity to the console | | ||
| ## How batching and deduplication work | ||
| Events queue in memory and flush on a timer or when the batch size fills. Duplicate events — defined as matching `eventType`, `linkId`, `referralCode`, and `memberId` — within the dedup window are dropped silently. | ||
| ```mermaid | ||
| graph TD | ||
| A["track() called"] --> B{"Duplicate?"} | ||
| B -- Yes --> C["Drop"] | ||
| B -- No --> D["Add to queue"] | ||
| D --> E{"Queue full?"} | ||
| E -- Yes --> F["Flush"] | ||
| E -- No --> G["Wait for timer"] | ||
| G --> F | ||
| ``` | ||
| ## Data privacy and security | ||
| - **Outbound only.** The SDK sends event data to the Bagdock API. It does not read cookies, fingerprint browsers, or collect PII. | ||
| - **Scoped API keys.** Use a key scoped to analytics write access. Never embed keys with broader permissions in client-side code. | ||
| - **Session-scoped UTM storage.** UTM data lives in `sessionStorage`, scoped to the current tab. It is cleared when the tab closes. | ||
| - **HTTPS enforced.** All API requests use `Authorization: Bearer` headers over TLS. | ||
| - **Zero dependencies.** No third-party runtime code. The SDK uses native `fetch` and `navigator.sendBeacon`. | ||
| ## License | ||
| MIT | ||
| MIT — see [LICENSE](LICENSE). |
29212
46.98%468
28.22%285
223.86%