@bagdock/analytics
Advanced tools
+23
-1
@@ -14,2 +14,10 @@ type EventType = 'click' | 'lead' | 'sale' | 'signup' | 'embed_render' | 'share' | 'qr_scan' | 'deep_link_open' | 'page_view' | 'reward_redeemed' | 'points_earned' | 'referral_completed'; | ||
| } | ||
| interface IdentifyParams { | ||
| /** Known contact/user ID (e.g. email, contact prefixed ID) */ | ||
| contactId: string; | ||
| /** Operator slug or ID for scoping */ | ||
| operatorId?: string; | ||
| /** Extra traits to persist on the contact (name, email, etc.) */ | ||
| traits?: Record<string, unknown>; | ||
| } | ||
| interface BagdockAnalyticsConfig { | ||
@@ -46,4 +54,18 @@ /** API key or embed token */ | ||
| private utm; | ||
| private _anonymousId; | ||
| constructor(config: BagdockAnalyticsConfig); | ||
| getUTM(): UTMParams; | ||
| get anonymousId(): string; | ||
| /** | ||
| * Stitch the current anonymous ID to a known contact. | ||
| * | ||
| * Fires a POST to the operator's identify relay endpoint so the | ||
| * backend can upsert `contact_anonymous_ids` and attach attribution | ||
| * data gathered before the visitor was identified. | ||
| * | ||
| * @param params.contactId Known contact or user ID | ||
| * @param params.operatorId Optional operator scope | ||
| * @param params.traits Extra contact traits (name, email, etc.) | ||
| */ | ||
| identify(params: IdentifyParams): Promise<void>; | ||
| track(event: TrackableEvent): void; | ||
@@ -74,2 +96,2 @@ trackClick(linkId: string, referralCode?: string): void; | ||
| export { BagdockAnalytics, type BagdockAnalyticsConfig, type EventType, type TrackableEvent, type UTMParams, parseUTM }; | ||
| export { BagdockAnalytics, type BagdockAnalyticsConfig, type EventType, type IdentifyParams, type TrackableEvent, type UTMParams, parseUTM }; |
+23
-1
@@ -14,2 +14,10 @@ type EventType = 'click' | 'lead' | 'sale' | 'signup' | 'embed_render' | 'share' | 'qr_scan' | 'deep_link_open' | 'page_view' | 'reward_redeemed' | 'points_earned' | 'referral_completed'; | ||
| } | ||
| interface IdentifyParams { | ||
| /** Known contact/user ID (e.g. email, contact prefixed ID) */ | ||
| contactId: string; | ||
| /** Operator slug or ID for scoping */ | ||
| operatorId?: string; | ||
| /** Extra traits to persist on the contact (name, email, etc.) */ | ||
| traits?: Record<string, unknown>; | ||
| } | ||
| interface BagdockAnalyticsConfig { | ||
@@ -46,4 +54,18 @@ /** API key or embed token */ | ||
| private utm; | ||
| private _anonymousId; | ||
| constructor(config: BagdockAnalyticsConfig); | ||
| getUTM(): UTMParams; | ||
| get anonymousId(): string; | ||
| /** | ||
| * Stitch the current anonymous ID to a known contact. | ||
| * | ||
| * Fires a POST to the operator's identify relay endpoint so the | ||
| * backend can upsert `contact_anonymous_ids` and attach attribution | ||
| * data gathered before the visitor was identified. | ||
| * | ||
| * @param params.contactId Known contact or user ID | ||
| * @param params.operatorId Optional operator scope | ||
| * @param params.traits Extra contact traits (name, email, etc.) | ||
| */ | ||
| identify(params: IdentifyParams): Promise<void>; | ||
| track(event: TrackableEvent): void; | ||
@@ -74,2 +96,2 @@ trackClick(linkId: string, referralCode?: string): void; | ||
| export { BagdockAnalytics, type BagdockAnalyticsConfig, type EventType, type TrackableEvent, type UTMParams, parseUTM }; | ||
| export { BagdockAnalytics, type BagdockAnalyticsConfig, type EventType, type IdentifyParams, type TrackableEvent, type UTMParams, parseUTM }; |
+103
-6
@@ -30,3 +30,5 @@ "use strict"; | ||
| try { | ||
| const params = new URL(url || window.location.href).searchParams; | ||
| const raw = url ?? (typeof window !== "undefined" ? window.location.href : ""); | ||
| const base = typeof window !== "undefined" ? window.location.origin : "http://localhost"; | ||
| const params = new URL(raw, base).searchParams; | ||
| const utm = {}; | ||
@@ -46,3 +48,4 @@ for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]) { | ||
| try { | ||
| const existing = JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| const parsed = JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| const existing = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; | ||
| sessionStorage.setItem(UTM_STORAGE_KEY, JSON.stringify({ ...existing, ...utm })); | ||
@@ -55,3 +58,4 @@ } catch { | ||
| try { | ||
| return JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| const parsed = JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; | ||
| } catch { | ||
@@ -61,2 +65,3 @@ return {}; | ||
| } | ||
| var ANON_ID_KEY = "bagdock_anon_id"; | ||
| var DEFAULT_BASE_URL = "https://loyalty-api.bagdock.com"; | ||
@@ -66,2 +71,53 @@ var DEFAULT_FLUSH_INTERVAL = 5e3; | ||
| var DEFAULT_DEDUP_WINDOW = 500; | ||
| function generateAnonId() { | ||
| if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID(); | ||
| return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; | ||
| } | ||
| function getCookie(name) { | ||
| if (typeof document === "undefined") return void 0; | ||
| const match = document.cookie.match(new RegExp(`(?:^|;)\\s*${name}=([^;]+)`)); | ||
| return match ? decodeURIComponent(match[1]) : void 0; | ||
| } | ||
| function setCrossDomainCookie(name, value) { | ||
| if (typeof document === "undefined") return; | ||
| const hostname = typeof window !== "undefined" ? window.location.hostname : ""; | ||
| const domain = hostname.endsWith("bagdock.com") ? ".bagdock.com" : void 0; | ||
| const maxAge = 60 * 60 * 24 * 400; | ||
| const parts = [ | ||
| `${name}=${encodeURIComponent(value)}`, | ||
| "path=/", | ||
| `max-age=${maxAge}`, | ||
| "samesite=lax" | ||
| ]; | ||
| if (domain) parts.push(`domain=${domain}`); | ||
| if (hostname !== "localhost") parts.push("secure"); | ||
| document.cookie = parts.join("; "); | ||
| } | ||
| function getOrCreateAnonId() { | ||
| const fromCookie = getCookie(ANON_ID_KEY); | ||
| if (fromCookie) { | ||
| try { | ||
| localStorage?.setItem(ANON_ID_KEY, fromCookie); | ||
| } catch { | ||
| } | ||
| return fromCookie; | ||
| } | ||
| if (typeof localStorage !== "undefined") { | ||
| try { | ||
| const existing = localStorage.getItem(ANON_ID_KEY); | ||
| if (existing) { | ||
| setCrossDomainCookie(ANON_ID_KEY, existing); | ||
| return existing; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| const id = generateAnonId(); | ||
| try { | ||
| localStorage?.setItem(ANON_ID_KEY, id); | ||
| } catch { | ||
| } | ||
| setCrossDomainCookie(ANON_ID_KEY, id); | ||
| return id; | ||
| } | ||
| var BagdockAnalytics = class { | ||
@@ -74,2 +130,3 @@ config; | ||
| utm = {}; | ||
| _anonymousId; | ||
| constructor(config) { | ||
@@ -85,2 +142,3 @@ this.config = { | ||
| }; | ||
| this._anonymousId = typeof window !== "undefined" ? getOrCreateAnonId() : generateAnonId(); | ||
| this.startFlushTimer(); | ||
@@ -91,6 +149,4 @@ if (typeof window !== "undefined") { | ||
| persistUTM(freshUTM); | ||
| this.utm = freshUTM; | ||
| } else { | ||
| this.utm = getPersistedUTM(); | ||
| } | ||
| this.utm = getPersistedUTM(); | ||
| window.addEventListener("beforeunload", () => this.flush()); | ||
@@ -108,2 +164,42 @@ if (typeof document !== "undefined") { | ||
| } | ||
| get anonymousId() { | ||
| return this._anonymousId; | ||
| } | ||
| /** | ||
| * Stitch the current anonymous ID to a known contact. | ||
| * | ||
| * Fires a POST to the operator's identify relay endpoint so the | ||
| * backend can upsert `contact_anonymous_ids` and attach attribution | ||
| * data gathered before the visitor was identified. | ||
| * | ||
| * @param params.contactId Known contact or user ID | ||
| * @param params.operatorId Optional operator scope | ||
| * @param params.traits Extra contact traits (name, email, etc.) | ||
| */ | ||
| async identify(params) { | ||
| const payload = { | ||
| anonymous_id: this._anonymousId, | ||
| contact_id: params.contactId, | ||
| operator_id: params.operatorId, | ||
| traits: params.traits, | ||
| utm: Object.keys(this.utm).length > 0 ? this.utm : void 0, | ||
| landing_page: typeof window !== "undefined" ? window.location.href : void 0, | ||
| referrer: typeof document !== "undefined" ? document.referrer : void 0 | ||
| }; | ||
| this.log("identify \u2192", payload.contact_id, payload.anonymous_id); | ||
| try { | ||
| const url = `${this.config.baseUrl}/api/identify`; | ||
| await fetch(url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Authorization": `Bearer ${this.config.apiKey}` | ||
| }, | ||
| body: JSON.stringify(payload), | ||
| keepalive: true | ||
| }); | ||
| } catch (err) { | ||
| this.log("identify error:", err); | ||
| } | ||
| } | ||
| track(event) { | ||
@@ -185,2 +281,3 @@ if (this.isDuplicate(event)) { | ||
| event_type: event.eventType, | ||
| anonymous_id: this._anonymousId, | ||
| link_id: event.linkId, | ||
@@ -187,0 +284,0 @@ member_id: event.memberId, |
+103
-6
@@ -5,3 +5,5 @@ // src/index.ts | ||
| try { | ||
| const params = new URL(url || window.location.href).searchParams; | ||
| const raw = url ?? (typeof window !== "undefined" ? window.location.href : ""); | ||
| const base = typeof window !== "undefined" ? window.location.origin : "http://localhost"; | ||
| const params = new URL(raw, base).searchParams; | ||
| const utm = {}; | ||
@@ -21,3 +23,4 @@ for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]) { | ||
| try { | ||
| const existing = JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| const parsed = JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| const existing = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; | ||
| sessionStorage.setItem(UTM_STORAGE_KEY, JSON.stringify({ ...existing, ...utm })); | ||
@@ -30,3 +33,4 @@ } catch { | ||
| try { | ||
| return JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| const parsed = JSON.parse(sessionStorage.getItem(UTM_STORAGE_KEY) || "{}"); | ||
| return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; | ||
| } catch { | ||
@@ -36,2 +40,3 @@ return {}; | ||
| } | ||
| var ANON_ID_KEY = "bagdock_anon_id"; | ||
| var DEFAULT_BASE_URL = "https://loyalty-api.bagdock.com"; | ||
@@ -41,2 +46,53 @@ var DEFAULT_FLUSH_INTERVAL = 5e3; | ||
| var DEFAULT_DEDUP_WINDOW = 500; | ||
| function generateAnonId() { | ||
| if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID(); | ||
| return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; | ||
| } | ||
| function getCookie(name) { | ||
| if (typeof document === "undefined") return void 0; | ||
| const match = document.cookie.match(new RegExp(`(?:^|;)\\s*${name}=([^;]+)`)); | ||
| return match ? decodeURIComponent(match[1]) : void 0; | ||
| } | ||
| function setCrossDomainCookie(name, value) { | ||
| if (typeof document === "undefined") return; | ||
| const hostname = typeof window !== "undefined" ? window.location.hostname : ""; | ||
| const domain = hostname.endsWith("bagdock.com") ? ".bagdock.com" : void 0; | ||
| const maxAge = 60 * 60 * 24 * 400; | ||
| const parts = [ | ||
| `${name}=${encodeURIComponent(value)}`, | ||
| "path=/", | ||
| `max-age=${maxAge}`, | ||
| "samesite=lax" | ||
| ]; | ||
| if (domain) parts.push(`domain=${domain}`); | ||
| if (hostname !== "localhost") parts.push("secure"); | ||
| document.cookie = parts.join("; "); | ||
| } | ||
| function getOrCreateAnonId() { | ||
| const fromCookie = getCookie(ANON_ID_KEY); | ||
| if (fromCookie) { | ||
| try { | ||
| localStorage?.setItem(ANON_ID_KEY, fromCookie); | ||
| } catch { | ||
| } | ||
| return fromCookie; | ||
| } | ||
| if (typeof localStorage !== "undefined") { | ||
| try { | ||
| const existing = localStorage.getItem(ANON_ID_KEY); | ||
| if (existing) { | ||
| setCrossDomainCookie(ANON_ID_KEY, existing); | ||
| return existing; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| const id = generateAnonId(); | ||
| try { | ||
| localStorage?.setItem(ANON_ID_KEY, id); | ||
| } catch { | ||
| } | ||
| setCrossDomainCookie(ANON_ID_KEY, id); | ||
| return id; | ||
| } | ||
| var BagdockAnalytics = class { | ||
@@ -49,2 +105,3 @@ config; | ||
| utm = {}; | ||
| _anonymousId; | ||
| constructor(config) { | ||
@@ -60,2 +117,3 @@ this.config = { | ||
| }; | ||
| this._anonymousId = typeof window !== "undefined" ? getOrCreateAnonId() : generateAnonId(); | ||
| this.startFlushTimer(); | ||
@@ -66,6 +124,4 @@ if (typeof window !== "undefined") { | ||
| persistUTM(freshUTM); | ||
| this.utm = freshUTM; | ||
| } else { | ||
| this.utm = getPersistedUTM(); | ||
| } | ||
| this.utm = getPersistedUTM(); | ||
| window.addEventListener("beforeunload", () => this.flush()); | ||
@@ -83,2 +139,42 @@ if (typeof document !== "undefined") { | ||
| } | ||
| get anonymousId() { | ||
| return this._anonymousId; | ||
| } | ||
| /** | ||
| * Stitch the current anonymous ID to a known contact. | ||
| * | ||
| * Fires a POST to the operator's identify relay endpoint so the | ||
| * backend can upsert `contact_anonymous_ids` and attach attribution | ||
| * data gathered before the visitor was identified. | ||
| * | ||
| * @param params.contactId Known contact or user ID | ||
| * @param params.operatorId Optional operator scope | ||
| * @param params.traits Extra contact traits (name, email, etc.) | ||
| */ | ||
| async identify(params) { | ||
| const payload = { | ||
| anonymous_id: this._anonymousId, | ||
| contact_id: params.contactId, | ||
| operator_id: params.operatorId, | ||
| traits: params.traits, | ||
| utm: Object.keys(this.utm).length > 0 ? this.utm : void 0, | ||
| landing_page: typeof window !== "undefined" ? window.location.href : void 0, | ||
| referrer: typeof document !== "undefined" ? document.referrer : void 0 | ||
| }; | ||
| this.log("identify \u2192", payload.contact_id, payload.anonymous_id); | ||
| try { | ||
| const url = `${this.config.baseUrl}/api/identify`; | ||
| await fetch(url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Authorization": `Bearer ${this.config.apiKey}` | ||
| }, | ||
| body: JSON.stringify(payload), | ||
| keepalive: true | ||
| }); | ||
| } catch (err) { | ||
| this.log("identify error:", err); | ||
| } | ||
| } | ||
| track(event) { | ||
@@ -160,2 +256,3 @@ if (this.isDuplicate(event)) { | ||
| event_type: event.eventType, | ||
| anonymous_id: this._anonymousId, | ||
| link_id: event.linkId, | ||
@@ -162,0 +259,0 @@ member_id: event.memberId, |
+1
-1
| { | ||
| "name": "@bagdock/analytics", | ||
| "version": "0.2.0", | ||
| "version": "0.3.0", | ||
| "description": "Bagdock Analytics SDK — lightweight client-side event tracking with batching and dedup", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
+51
-3
@@ -17,3 +17,3 @@ ``` | ||
| 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. | ||
| Track events, identify visitors, 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, stitches anonymous visitors to known contacts, and flushes gracefully on page unload. | ||
@@ -44,3 +44,3 @@ [](https://www.npmjs.com/package/@bagdock/analytics) | ||
| SDK->>SDK: Dedup check | ||
| SDK->>SDK: Attach UTM metadata | ||
| SDK->>SDK: Attach UTM + anonymous_id | ||
| SDK->>SDK: Queue event | ||
@@ -52,2 +52,8 @@ | ||
| API-->>SDK: 200 OK | ||
| Note over App: User logs in or completes checkout | ||
| App->>SDK: identify({ contactId: 'ct_abc' }) | ||
| SDK->>API: POST /api/identify | ||
| API-->>SDK: 200 OK { stitched } | ||
| ``` | ||
@@ -197,2 +203,30 @@ | ||
| ## How to identify visitors | ||
| When a visitor logs in, completes checkout, or submits a form, call `identify()` to stitch their anonymous browsing history to a known contact record. The SDK sends the anonymous ID to the Bagdock identify relay, which upserts a `contact_anonymous_ids` row in the operator database and backfills first-touch UTM attribution. | ||
| ```typescript | ||
| const analytics = new BagdockAnalytics({ apiKey: 'YOUR_API_KEY' }) | ||
| // After login / checkout / form submission | ||
| await analytics.identify({ | ||
| contactId: 'ct_abc123', | ||
| operatorId: 'opreg_acme', | ||
| traits: { | ||
| email: 'jane@example.com', | ||
| firstName: 'Jane', | ||
| }, | ||
| }) | ||
| ``` | ||
| The anonymous ID is generated on first visit and persisted in both `localStorage` and a cross-subdomain cookie (`bagdock_anon_id`, `.bagdock.com`, 400-day TTL). This ensures the same anonymous ID follows a visitor across `bagdock.com` subdomains — marketing site, checkout, customer app — so pre-identify page views are attributed correctly after the visitor is identified. | ||
| ```typescript | ||
| // Read the anonymous ID at any time | ||
| analytics.anonymousId | ||
| // → "a1b2c3d4-e5f6-7890-abcd-ef1234567890" | ||
| ``` | ||
| --- | ||
| ## Methods | ||
@@ -208,2 +242,4 @@ | ||
| | `trackEmbedRender(operatorId?)` | Track when an embedded widget renders | | ||
| | `identify(params)` | Stitch the current anonymous ID to a known contact | | ||
| | `anonymousId` | Getter — the current anonymous ID (generated or restored) | | ||
| | `getUTM()` | Return the current UTM attribution context | | ||
@@ -241,2 +277,12 @@ | `flush()` | Flush the event queue immediately | | ||
| ### `IdentifyParams` | ||
| ```typescript | ||
| interface IdentifyParams { | ||
| contactId: string | ||
| operatorId?: string | ||
| traits?: Record<string, unknown> | ||
| } | ||
| ``` | ||
| ### Exports | ||
@@ -250,2 +296,3 @@ | ||
| | `TrackableEvent` | interface | Event payload shape | | ||
| | `IdentifyParams` | interface | Parameters for `identify()` | | ||
| | `EventType` | type | Union of supported event type strings | | ||
@@ -283,3 +330,4 @@ | `UTMParams` | interface | UTM parameter shape | | ||
| - **Outbound only.** The SDK sends event data to the Bagdock API. It does not read cookies, fingerprint browsers, or collect PII. | ||
| - **Outbound only.** The SDK sends event data to the Bagdock API. It does not fingerprint browsers or collect PII beyond what you explicitly pass to `identify()`. | ||
| - **Anonymous ID cookie.** A `bagdock_anon_id` cookie is set on `.bagdock.com` with `SameSite=lax` and a 400-day TTL. It contains a random UUID used solely for identity stitching — no PII. On non-Bagdock domains or localhost the cookie is scoped to the current host. | ||
| - **Scoped API keys.** Use a key scoped to analytics write access. Never embed keys with broader permissions in client-side code. | ||
@@ -286,0 +334,0 @@ - **Session-scoped UTM storage.** UTM data lives in `sessionStorage`, scoped to the current tab. It is cleared when the tab closes. |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
40010
36.96%684
46.15%333
16.84%4
100%