Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@bagdock/analytics

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bagdock/analytics - npm Package Compare versions

Comparing version
0.2.0
to
0.3.0
+23
-1
dist/index.d.mts

@@ -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 };

@@ -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 };

@@ -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,

@@ -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",

@@ -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 @@ [![npm version](https://img.shields.io/npm/v/@bagdock/analytics.svg)](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.