@outlit/browser 0.1.0

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.
@@ -0,0 +1,487 @@
1
+ // src/react/provider.tsx
2
+ import { createContext, useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ // src/tracker.ts
5
+ import {
6
+ DEFAULT_API_HOST,
7
+ buildCustomEvent,
8
+ buildFormEvent,
9
+ buildIdentifyEvent,
10
+ buildIngestPayload,
11
+ buildPageviewEvent
12
+ } from "@outlit/core";
13
+
14
+ // src/autocapture.ts
15
+ import { extractIdentityFromForm, sanitizeFormFields } from "@outlit/core";
16
+ var pageviewCallback = null;
17
+ var lastUrl = null;
18
+ function initPageviewTracking(callback) {
19
+ pageviewCallback = callback;
20
+ capturePageview();
21
+ setupSpaListeners();
22
+ }
23
+ function capturePageview() {
24
+ if (!pageviewCallback) return;
25
+ const url = window.location.href;
26
+ const referrer = document.referrer;
27
+ const title = document.title;
28
+ if (url === lastUrl) return;
29
+ lastUrl = url;
30
+ pageviewCallback(url, referrer, title);
31
+ }
32
+ function setupSpaListeners() {
33
+ window.addEventListener("popstate", () => {
34
+ capturePageview();
35
+ });
36
+ const originalPushState = history.pushState;
37
+ const originalReplaceState = history.replaceState;
38
+ history.pushState = function(...args) {
39
+ originalPushState.apply(this, args);
40
+ capturePageview();
41
+ };
42
+ history.replaceState = function(...args) {
43
+ originalReplaceState.apply(this, args);
44
+ capturePageview();
45
+ };
46
+ }
47
+ var formCallback = null;
48
+ var formDenylist;
49
+ var identityCallback = null;
50
+ function initFormTracking(callback, denylist, onIdentity) {
51
+ formCallback = callback;
52
+ formDenylist = denylist;
53
+ identityCallback = onIdentity ?? null;
54
+ document.addEventListener("submit", handleFormSubmit, true);
55
+ }
56
+ function handleFormSubmit(event) {
57
+ if (!formCallback) return;
58
+ const form = event.target;
59
+ if (!(form instanceof HTMLFormElement)) return;
60
+ const url = window.location.href;
61
+ const formId = form.id || form.name || void 0;
62
+ const formData = new FormData(form);
63
+ const fields = {};
64
+ const inputTypes = /* @__PURE__ */ new Map();
65
+ const inputs = form.querySelectorAll("input, select, textarea");
66
+ for (const input of inputs) {
67
+ const name = input.getAttribute("name");
68
+ if (name && input instanceof HTMLInputElement) {
69
+ inputTypes.set(name, input.type);
70
+ }
71
+ }
72
+ formData.forEach((value, key) => {
73
+ if (typeof value === "string") {
74
+ fields[key] = value;
75
+ }
76
+ });
77
+ const sanitizedFields = sanitizeFormFields(fields, formDenylist);
78
+ if (identityCallback) {
79
+ const identity = extractIdentityFromForm(fields, inputTypes);
80
+ if (identity) {
81
+ identityCallback(identity);
82
+ }
83
+ }
84
+ if (sanitizedFields && Object.keys(sanitizedFields).length > 0) {
85
+ formCallback(url, formId, sanitizedFields);
86
+ }
87
+ }
88
+ function stopAutocapture() {
89
+ pageviewCallback = null;
90
+ formCallback = null;
91
+ identityCallback = null;
92
+ document.removeEventListener("submit", handleFormSubmit, true);
93
+ }
94
+
95
+ // src/storage.ts
96
+ var VISITOR_ID_KEY = "outlit_visitor_id";
97
+ function generateVisitorId() {
98
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
99
+ return crypto.randomUUID();
100
+ }
101
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
102
+ const r = Math.random() * 16 | 0;
103
+ const v = c === "x" ? r : r & 3 | 8;
104
+ return v.toString(16);
105
+ });
106
+ }
107
+ function getOrCreateVisitorId() {
108
+ try {
109
+ const stored = localStorage.getItem(VISITOR_ID_KEY);
110
+ if (stored && isValidUuid(stored)) {
111
+ return stored;
112
+ }
113
+ } catch {
114
+ }
115
+ const cookieValue = getCookie(VISITOR_ID_KEY);
116
+ if (cookieValue && isValidUuid(cookieValue)) {
117
+ try {
118
+ localStorage.setItem(VISITOR_ID_KEY, cookieValue);
119
+ } catch {
120
+ }
121
+ return cookieValue;
122
+ }
123
+ const visitorId = generateVisitorId();
124
+ persistVisitorId(visitorId);
125
+ return visitorId;
126
+ }
127
+ function persistVisitorId(visitorId) {
128
+ try {
129
+ localStorage.setItem(VISITOR_ID_KEY, visitorId);
130
+ } catch {
131
+ }
132
+ setCookie(VISITOR_ID_KEY, visitorId, 365);
133
+ }
134
+ function isValidUuid(value) {
135
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
136
+ }
137
+ function getCookie(name) {
138
+ if (typeof document === "undefined") return null;
139
+ const value = `; ${document.cookie}`;
140
+ const parts = value.split(`; ${name}=`);
141
+ if (parts.length === 2) {
142
+ return parts.pop()?.split(";").shift() ?? null;
143
+ }
144
+ return null;
145
+ }
146
+ function getRootDomain() {
147
+ if (typeof window === "undefined") return null;
148
+ const hostname = window.location.hostname;
149
+ if (hostname === "localhost" || /^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
150
+ return null;
151
+ }
152
+ const parts = hostname.split(".");
153
+ if (parts.length >= 2) {
154
+ const twoPartTlds = ["co.uk", "com.au", "co.nz", "org.uk", "net.au", "com.br"];
155
+ const lastTwo = parts.slice(-2).join(".");
156
+ if (twoPartTlds.includes(lastTwo) && parts.length >= 3) {
157
+ return parts.slice(-3).join(".");
158
+ }
159
+ return parts.slice(-2).join(".");
160
+ }
161
+ return null;
162
+ }
163
+ function setCookie(name, value, days) {
164
+ if (typeof document === "undefined") return;
165
+ const expires = /* @__PURE__ */ new Date();
166
+ expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1e3);
167
+ let cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
168
+ const rootDomain = getRootDomain();
169
+ if (rootDomain) {
170
+ cookie += `;domain=${rootDomain}`;
171
+ }
172
+ document.cookie = cookie;
173
+ }
174
+
175
+ // src/tracker.ts
176
+ var Tracker = class {
177
+ publicKey;
178
+ apiHost;
179
+ visitorId = null;
180
+ eventQueue = [];
181
+ flushTimer = null;
182
+ flushInterval;
183
+ isInitialized = false;
184
+ isTrackingEnabled = false;
185
+ options;
186
+ constructor(options) {
187
+ this.publicKey = options.publicKey;
188
+ this.apiHost = options.apiHost ?? DEFAULT_API_HOST;
189
+ this.flushInterval = options.flushInterval ?? 5e3;
190
+ this.options = options;
191
+ if (typeof window !== "undefined") {
192
+ window.addEventListener("beforeunload", () => {
193
+ this.flush();
194
+ });
195
+ }
196
+ this.isInitialized = true;
197
+ if (options.autoTrack !== false) {
198
+ this.enableTracking();
199
+ }
200
+ }
201
+ // ============================================
202
+ // PUBLIC API
203
+ // ============================================
204
+ /**
205
+ * Enable tracking. Call this after obtaining user consent.
206
+ * This will:
207
+ * - Generate/retrieve the visitor ID
208
+ * - Start automatic pageview and form tracking (if configured)
209
+ * - Begin sending events to the server
210
+ *
211
+ * If autoTrack is true (default), this is called automatically on init.
212
+ */
213
+ enableTracking() {
214
+ if (this.isTrackingEnabled) {
215
+ return;
216
+ }
217
+ this.visitorId = getOrCreateVisitorId();
218
+ this.startFlushTimer();
219
+ if (this.options.trackPageviews !== false) {
220
+ this.initPageviewTracking();
221
+ }
222
+ if (this.options.trackForms !== false) {
223
+ this.initFormTracking(this.options.formFieldDenylist);
224
+ }
225
+ this.isTrackingEnabled = true;
226
+ }
227
+ /**
228
+ * Check if tracking is currently enabled.
229
+ */
230
+ isEnabled() {
231
+ return this.isTrackingEnabled;
232
+ }
233
+ /**
234
+ * Track a custom event.
235
+ */
236
+ track(eventName, properties) {
237
+ if (!this.isTrackingEnabled) {
238
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
239
+ return;
240
+ }
241
+ const event = buildCustomEvent({
242
+ url: window.location.href,
243
+ referrer: document.referrer,
244
+ eventName,
245
+ properties
246
+ });
247
+ this.enqueue(event);
248
+ }
249
+ /**
250
+ * Identify the current visitor.
251
+ * Links the anonymous visitor to a known user.
252
+ */
253
+ identify(options) {
254
+ if (!this.isTrackingEnabled) {
255
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
256
+ return;
257
+ }
258
+ const event = buildIdentifyEvent({
259
+ url: window.location.href,
260
+ referrer: document.referrer,
261
+ email: options.email,
262
+ userId: options.userId,
263
+ traits: options.traits
264
+ });
265
+ this.enqueue(event);
266
+ }
267
+ /**
268
+ * Get the current visitor ID.
269
+ * Returns null if tracking is not enabled.
270
+ */
271
+ getVisitorId() {
272
+ return this.visitorId;
273
+ }
274
+ /**
275
+ * Manually flush the event queue.
276
+ */
277
+ async flush() {
278
+ if (this.eventQueue.length === 0) return;
279
+ const events = [...this.eventQueue];
280
+ this.eventQueue = [];
281
+ await this.sendEvents(events);
282
+ }
283
+ /**
284
+ * Shutdown the tracker.
285
+ */
286
+ async shutdown() {
287
+ if (this.flushTimer) {
288
+ clearInterval(this.flushTimer);
289
+ this.flushTimer = null;
290
+ }
291
+ stopAutocapture();
292
+ await this.flush();
293
+ }
294
+ // ============================================
295
+ // INTERNAL METHODS
296
+ // ============================================
297
+ initPageviewTracking() {
298
+ initPageviewTracking((url, referrer, title) => {
299
+ const event = buildPageviewEvent({ url, referrer, title });
300
+ this.enqueue(event);
301
+ });
302
+ }
303
+ initFormTracking(denylist) {
304
+ const identityCallback2 = this.options.autoIdentify !== false ? (identity) => {
305
+ const traits = {};
306
+ if (identity.name) traits.name = identity.name;
307
+ if (identity.firstName) traits.firstName = identity.firstName;
308
+ if (identity.lastName) traits.lastName = identity.lastName;
309
+ this.identify({
310
+ email: identity.email,
311
+ traits: Object.keys(traits).length > 0 ? traits : void 0
312
+ });
313
+ } : void 0;
314
+ initFormTracking(
315
+ (url, formId, fields) => {
316
+ const event = buildFormEvent({
317
+ url,
318
+ referrer: document.referrer,
319
+ formId,
320
+ formFields: fields
321
+ });
322
+ this.enqueue(event);
323
+ },
324
+ denylist,
325
+ identityCallback2
326
+ );
327
+ }
328
+ enqueue(event) {
329
+ this.eventQueue.push(event);
330
+ if (this.eventQueue.length >= 10) {
331
+ this.flush();
332
+ }
333
+ }
334
+ startFlushTimer() {
335
+ if (this.flushTimer) return;
336
+ this.flushTimer = setInterval(() => {
337
+ this.flush();
338
+ }, this.flushInterval);
339
+ }
340
+ async sendEvents(events) {
341
+ if (events.length === 0) return;
342
+ if (!this.visitorId) return;
343
+ const payload = buildIngestPayload(this.visitorId, "pixel", events);
344
+ const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`;
345
+ try {
346
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
347
+ const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
348
+ const sent = navigator.sendBeacon(url, blob);
349
+ if (sent) return;
350
+ }
351
+ await fetch(url, {
352
+ method: "POST",
353
+ headers: {
354
+ "Content-Type": "application/json"
355
+ },
356
+ body: JSON.stringify(payload),
357
+ keepalive: true
358
+ });
359
+ } catch (error) {
360
+ console.warn("[Outlit] Failed to send events:", error);
361
+ }
362
+ }
363
+ };
364
+
365
+ // src/react/provider.tsx
366
+ import { jsx } from "react/jsx-runtime";
367
+ var OutlitContext = createContext({
368
+ tracker: null,
369
+ isInitialized: false,
370
+ isTrackingEnabled: false,
371
+ enableTracking: () => {
372
+ }
373
+ });
374
+ function OutlitProvider({
375
+ children,
376
+ publicKey,
377
+ apiHost,
378
+ trackPageviews = true,
379
+ trackForms = true,
380
+ formFieldDenylist,
381
+ flushInterval,
382
+ autoTrack = true,
383
+ autoIdentify = true
384
+ }) {
385
+ const trackerRef = useRef(null);
386
+ const initializedRef = useRef(false);
387
+ const [isTrackingEnabled, setIsTrackingEnabled] = useState(false);
388
+ useEffect(() => {
389
+ if (initializedRef.current) return;
390
+ trackerRef.current = new Tracker({
391
+ publicKey,
392
+ apiHost,
393
+ trackPageviews,
394
+ trackForms,
395
+ formFieldDenylist,
396
+ flushInterval,
397
+ autoTrack,
398
+ autoIdentify
399
+ });
400
+ initializedRef.current = true;
401
+ setIsTrackingEnabled(trackerRef.current.isEnabled());
402
+ return () => {
403
+ trackerRef.current?.shutdown();
404
+ };
405
+ }, [
406
+ publicKey,
407
+ apiHost,
408
+ trackPageviews,
409
+ trackForms,
410
+ formFieldDenylist,
411
+ flushInterval,
412
+ autoTrack,
413
+ autoIdentify
414
+ ]);
415
+ const enableTracking = useCallback(() => {
416
+ if (trackerRef.current) {
417
+ trackerRef.current.enableTracking();
418
+ setIsTrackingEnabled(true);
419
+ }
420
+ }, []);
421
+ return /* @__PURE__ */ jsx(
422
+ OutlitContext.Provider,
423
+ {
424
+ value: {
425
+ tracker: trackerRef.current,
426
+ isInitialized: initializedRef.current,
427
+ isTrackingEnabled,
428
+ enableTracking
429
+ },
430
+ children
431
+ }
432
+ );
433
+ }
434
+
435
+ // src/react/hooks.ts
436
+ import { useCallback as useCallback2, useContext } from "react";
437
+ function useOutlit() {
438
+ const { tracker, isInitialized, isTrackingEnabled, enableTracking } = useContext(OutlitContext);
439
+ const track = useCallback2(
440
+ (eventName, properties) => {
441
+ if (!tracker) {
442
+ console.warn("[Outlit] Tracker not initialized. Make sure OutlitProvider is mounted.");
443
+ return;
444
+ }
445
+ tracker.track(eventName, properties);
446
+ },
447
+ [tracker]
448
+ );
449
+ const identify = useCallback2(
450
+ (options) => {
451
+ if (!tracker) {
452
+ console.warn("[Outlit] Tracker not initialized. Make sure OutlitProvider is mounted.");
453
+ return;
454
+ }
455
+ tracker.identify(options);
456
+ },
457
+ [tracker]
458
+ );
459
+ const getVisitorId = useCallback2(() => {
460
+ if (!tracker) return null;
461
+ return tracker.getVisitorId();
462
+ }, [tracker]);
463
+ return {
464
+ track,
465
+ identify,
466
+ getVisitorId,
467
+ isInitialized,
468
+ isTrackingEnabled,
469
+ enableTracking
470
+ };
471
+ }
472
+ function useTrack() {
473
+ const { track } = useOutlit();
474
+ return track;
475
+ }
476
+ function useIdentify() {
477
+ const { identify } = useOutlit();
478
+ return identify;
479
+ }
480
+ export {
481
+ OutlitContext,
482
+ OutlitProvider,
483
+ useIdentify,
484
+ useOutlit,
485
+ useTrack
486
+ };
487
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/provider.tsx","../../src/tracker.ts","../../src/autocapture.ts","../../src/storage.ts","../../src/react/hooks.ts"],"sourcesContent":["import { type ReactNode, createContext, useCallback, useEffect, useRef, useState } from \"react\"\nimport { Tracker, type TrackerOptions } from \"../tracker\"\n\n// ============================================\n// CONTEXT\n// ============================================\n\nexport interface OutlitContextValue {\n tracker: Tracker | null\n isInitialized: boolean\n isTrackingEnabled: boolean\n enableTracking: () => void\n}\n\nexport const OutlitContext = createContext<OutlitContextValue>({\n tracker: null,\n isInitialized: false,\n isTrackingEnabled: false,\n enableTracking: () => {},\n})\n\n// ============================================\n// PROVIDER\n// ============================================\n\nexport interface OutlitProviderProps extends Omit<TrackerOptions, \"trackPageviews\"> {\n children: ReactNode\n /**\n * Whether to automatically track pageviews.\n * When true (default), tracks pageviews on route changes.\n */\n trackPageviews?: boolean\n /**\n * Whether to start tracking automatically on mount.\n * Set to false if you need to wait for user consent.\n * Call enableTracking() (from useOutlit hook) after consent is obtained.\n * @default true\n */\n autoTrack?: boolean\n}\n\n/**\n * Outlit Provider component.\n * Initializes the tracker and provides it to child components via context.\n *\n * @example\n * ```tsx\n * // layout.tsx - Auto tracking (default)\n * import { OutlitProvider } from '@outlit/tracker/react'\n *\n * export default function RootLayout({ children }) {\n * return (\n * <OutlitProvider publicKey=\"pk_xxx\" trackPageviews>\n * {children}\n * </OutlitProvider>\n * )\n * }\n * ```\n *\n * @example\n * ```tsx\n * // layout.tsx - With consent management\n * import { OutlitProvider } from '@outlit/tracker/react'\n *\n * export default function RootLayout({ children }) {\n * return (\n * <OutlitProvider publicKey=\"pk_xxx\" autoTrack={false}>\n * {children}\n * </OutlitProvider>\n * )\n * }\n *\n * // ConsentBanner.tsx\n * import { useOutlit } from '@outlit/tracker/react'\n *\n * function ConsentBanner() {\n * const { enableTracking } = useOutlit()\n * return <button onClick={enableTracking}>Accept Cookies</button>\n * }\n * ```\n */\nexport function OutlitProvider({\n children,\n publicKey,\n apiHost,\n trackPageviews = true,\n trackForms = true,\n formFieldDenylist,\n flushInterval,\n autoTrack = true,\n autoIdentify = true,\n}: OutlitProviderProps) {\n const trackerRef = useRef<Tracker | null>(null)\n const initializedRef = useRef(false)\n const [isTrackingEnabled, setIsTrackingEnabled] = useState(false)\n\n // Initialize tracker once\n useEffect(() => {\n if (initializedRef.current) return\n\n trackerRef.current = new Tracker({\n publicKey,\n apiHost,\n trackPageviews,\n trackForms,\n formFieldDenylist,\n flushInterval,\n autoTrack,\n autoIdentify,\n })\n\n initializedRef.current = true\n setIsTrackingEnabled(trackerRef.current.isEnabled())\n\n // Cleanup on unmount\n return () => {\n trackerRef.current?.shutdown()\n }\n }, [\n publicKey,\n apiHost,\n trackPageviews,\n trackForms,\n formFieldDenylist,\n flushInterval,\n autoTrack,\n autoIdentify,\n ])\n\n const enableTracking = useCallback(() => {\n if (trackerRef.current) {\n trackerRef.current.enableTracking()\n setIsTrackingEnabled(true)\n }\n }, [])\n\n return (\n <OutlitContext.Provider\n value={{\n tracker: trackerRef.current,\n isInitialized: initializedRef.current,\n isTrackingEnabled,\n enableTracking,\n }}\n >\n {children}\n </OutlitContext.Provider>\n )\n}\n","import {\n type BrowserIdentifyOptions,\n type BrowserTrackOptions,\n DEFAULT_API_HOST,\n type TrackerConfig,\n type TrackerEvent,\n buildCustomEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildIngestPayload,\n buildPageviewEvent,\n} from \"@outlit/core\"\nimport { initFormTracking, initPageviewTracking, stopAutocapture } from \"./autocapture\"\nimport { getOrCreateVisitorId } from \"./storage\"\n\n// ============================================\n// TRACKER CLASS\n// ============================================\n\nexport interface TrackerOptions extends TrackerConfig {\n /**\n * Automatically start tracking on init.\n * Set to false if you need to wait for user consent before tracking.\n * Call enableTracking() to start tracking after consent is obtained.\n * @default true\n */\n autoTrack?: boolean\n trackPageviews?: boolean\n trackForms?: boolean\n formFieldDenylist?: string[]\n flushInterval?: number\n /**\n * Automatically identify users when they submit forms with email fields.\n * Extracts email and name (first/last) from form fields using heuristics.\n * @default true\n */\n autoIdentify?: boolean\n}\n\nexport class Tracker {\n private publicKey: string\n private apiHost: string\n private visitorId: string | null = null\n private eventQueue: TrackerEvent[] = []\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushInterval: number\n private isInitialized = false\n private isTrackingEnabled = false\n private options: TrackerOptions\n\n constructor(options: TrackerOptions) {\n this.publicKey = options.publicKey\n this.apiHost = options.apiHost ?? DEFAULT_API_HOST\n this.flushInterval = options.flushInterval ?? 5000\n this.options = options\n\n // Set up beforeunload handler\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"beforeunload\", () => {\n this.flush()\n })\n }\n\n this.isInitialized = true\n\n // Start tracking immediately unless autoTrack is explicitly false\n if (options.autoTrack !== false) {\n this.enableTracking()\n }\n }\n\n // ============================================\n // PUBLIC API\n // ============================================\n\n /**\n * Enable tracking. Call this after obtaining user consent.\n * This will:\n * - Generate/retrieve the visitor ID\n * - Start automatic pageview and form tracking (if configured)\n * - Begin sending events to the server\n *\n * If autoTrack is true (default), this is called automatically on init.\n */\n enableTracking(): void {\n if (this.isTrackingEnabled) {\n return // Already enabled\n }\n\n // Now we can generate/retrieve the visitor ID (sets cookies/localStorage)\n this.visitorId = getOrCreateVisitorId()\n\n // Start the flush timer\n this.startFlushTimer()\n\n // Initialize autocapture if enabled\n if (this.options.trackPageviews !== false) {\n this.initPageviewTracking()\n }\n\n if (this.options.trackForms !== false) {\n this.initFormTracking(this.options.formFieldDenylist)\n }\n\n this.isTrackingEnabled = true\n }\n\n /**\n * Check if tracking is currently enabled.\n */\n isEnabled(): boolean {\n return this.isTrackingEnabled\n }\n\n /**\n * Track a custom event.\n */\n track(eventName: string, properties?: BrowserTrackOptions[\"properties\"]): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\n }\n\n const event = buildCustomEvent({\n url: window.location.href,\n referrer: document.referrer,\n eventName,\n properties,\n })\n this.enqueue(event)\n }\n\n /**\n * Identify the current visitor.\n * Links the anonymous visitor to a known user.\n */\n identify(options: BrowserIdentifyOptions): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\n }\n\n const event = buildIdentifyEvent({\n url: window.location.href,\n referrer: document.referrer,\n email: options.email,\n userId: options.userId,\n traits: options.traits,\n })\n this.enqueue(event)\n }\n\n /**\n * Get the current visitor ID.\n * Returns null if tracking is not enabled.\n */\n getVisitorId(): string | null {\n return this.visitorId\n }\n\n /**\n * Manually flush the event queue.\n */\n async flush(): Promise<void> {\n if (this.eventQueue.length === 0) return\n\n const events = [...this.eventQueue]\n this.eventQueue = []\n\n await this.sendEvents(events)\n }\n\n /**\n * Shutdown the tracker.\n */\n async shutdown(): Promise<void> {\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n stopAutocapture()\n await this.flush()\n }\n\n // ============================================\n // INTERNAL METHODS\n // ============================================\n\n private initPageviewTracking(): void {\n initPageviewTracking((url, referrer, title) => {\n const event = buildPageviewEvent({ url, referrer, title })\n this.enqueue(event)\n })\n }\n\n private initFormTracking(denylist?: string[]): void {\n // Create identity callback if autoIdentify is enabled (default: true)\n const identityCallback =\n this.options.autoIdentify !== false\n ? (identity: { email: string; name?: string; firstName?: string; lastName?: string }) => {\n // Build traits from extracted name fields\n const traits: Record<string, string> = {}\n if (identity.name) traits.name = identity.name\n if (identity.firstName) traits.firstName = identity.firstName\n if (identity.lastName) traits.lastName = identity.lastName\n\n this.identify({\n email: identity.email,\n traits: Object.keys(traits).length > 0 ? traits : undefined,\n })\n }\n : undefined\n\n initFormTracking(\n (url, formId, fields) => {\n const event = buildFormEvent({\n url,\n referrer: document.referrer,\n formId,\n formFields: fields,\n })\n this.enqueue(event)\n },\n denylist,\n identityCallback,\n )\n }\n\n private enqueue(event: TrackerEvent): void {\n this.eventQueue.push(event)\n\n // Flush immediately if queue is getting large\n if (this.eventQueue.length >= 10) {\n this.flush()\n }\n }\n\n private startFlushTimer(): void {\n if (this.flushTimer) return\n\n this.flushTimer = setInterval(() => {\n this.flush()\n }, this.flushInterval)\n }\n\n private async sendEvents(events: TrackerEvent[]): Promise<void> {\n if (events.length === 0) return\n if (!this.visitorId) return // Can't send without a visitor ID\n\n const payload = buildIngestPayload(this.visitorId, \"pixel\", events)\n const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`\n\n try {\n // Use sendBeacon for better reliability on page unload\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n const blob = new Blob([JSON.stringify(payload)], { type: \"application/json\" })\n const sent = navigator.sendBeacon(url, blob)\n if (sent) return\n }\n\n // Fallback to fetch\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n keepalive: true,\n })\n } catch (error) {\n // Silently fail - we don't want to break the user's site\n console.warn(\"[Outlit] Failed to send events:\", error)\n }\n }\n}\n\n// ============================================\n// SINGLETON INSTANCE\n// ============================================\n\nlet instance: Tracker | null = null\n\n/**\n * Initialize the Outlit tracker.\n * Should be called once at app startup.\n */\nexport function init(options: TrackerOptions): Tracker {\n if (instance) {\n console.warn(\"[Outlit] Tracker already initialized\")\n return instance\n }\n\n instance = new Tracker(options)\n return instance\n}\n\n/**\n * Get the tracker instance.\n * Throws if not initialized.\n */\nexport function getInstance(): Tracker {\n if (!instance) {\n throw new Error(\"[Outlit] Tracker not initialized. Call init() first.\")\n }\n return instance\n}\n\n/**\n * Track a custom event.\n * Convenience method that uses the singleton instance.\n */\nexport function track(eventName: string, properties?: BrowserTrackOptions[\"properties\"]): void {\n getInstance().track(eventName, properties)\n}\n\n/**\n * Identify the current visitor.\n * Convenience method that uses the singleton instance.\n */\nexport function identify(options: BrowserIdentifyOptions): void {\n getInstance().identify(options)\n}\n\n/**\n * Enable tracking after consent is obtained.\n * Call this in your consent management tool's callback.\n * Convenience method that uses the singleton instance.\n */\nexport function enableTracking(): void {\n getInstance().enableTracking()\n}\n\n/**\n * Check if tracking is currently enabled.\n * Convenience method that uses the singleton instance.\n */\nexport function isTrackingEnabled(): boolean {\n return getInstance().isEnabled()\n}\n","import { type ExtractedIdentity, extractIdentityFromForm, sanitizeFormFields } from \"@outlit/core\"\n\n// ============================================\n// PAGEVIEW TRACKING\n// ============================================\n\ntype PageviewCallback = (url: string, referrer: string, title: string) => void\n\nlet pageviewCallback: PageviewCallback | null = null\nlet lastUrl: string | null = null\n\n/**\n * Initialize automatic pageview tracking.\n * Captures initial pageview and listens for SPA navigation.\n */\nexport function initPageviewTracking(callback: PageviewCallback): void {\n pageviewCallback = callback\n\n // Capture initial pageview\n capturePageview()\n\n // Listen for SPA navigation\n setupSpaListeners()\n}\n\n/**\n * Capture a pageview event.\n */\nfunction capturePageview(): void {\n if (!pageviewCallback) return\n\n const url = window.location.href\n const referrer = document.referrer\n const title = document.title\n\n // Avoid duplicate pageviews for the same URL\n if (url === lastUrl) return\n lastUrl = url\n\n pageviewCallback(url, referrer, title)\n}\n\n/**\n * Set up listeners for SPA navigation.\n */\nfunction setupSpaListeners(): void {\n // Listen for popstate (browser back/forward)\n window.addEventListener(\"popstate\", () => {\n capturePageview()\n })\n\n // Monkey-patch pushState and replaceState\n const originalPushState = history.pushState\n const originalReplaceState = history.replaceState\n\n history.pushState = function (...args) {\n originalPushState.apply(this, args)\n capturePageview()\n }\n\n history.replaceState = function (...args) {\n originalReplaceState.apply(this, args)\n capturePageview()\n }\n}\n\n// ============================================\n// FORM TRACKING\n// ============================================\n\ntype FormCallback = (\n url: string,\n formId: string | undefined,\n fields: Record<string, string>,\n) => void\n\ntype IdentityCallback = (identity: ExtractedIdentity) => void\n\nlet formCallback: FormCallback | null = null\nlet formDenylist: string[] | undefined\nlet identityCallback: IdentityCallback | null = null\n\n/**\n * Initialize automatic form tracking.\n * Captures form submissions with field sanitization.\n *\n * @param callback - Called when a form is submitted with sanitized fields\n * @param denylist - Optional list of field names to exclude\n * @param onIdentity - Optional callback for auto-identification when email is found\n */\nexport function initFormTracking(\n callback: FormCallback,\n denylist?: string[],\n onIdentity?: IdentityCallback,\n): void {\n formCallback = callback\n formDenylist = denylist\n identityCallback = onIdentity ?? null\n\n // Listen for form submissions\n document.addEventListener(\"submit\", handleFormSubmit, true)\n}\n\n/**\n * Handle form submission events.\n */\nfunction handleFormSubmit(event: Event): void {\n if (!formCallback) return\n\n const form = event.target as HTMLFormElement\n if (!(form instanceof HTMLFormElement)) return\n\n const url = window.location.href\n const formId = form.id || form.name || undefined\n\n // Extract form fields and input types\n const formData = new FormData(form)\n const fields: Record<string, string> = {}\n const inputTypes = new Map<string, string>()\n\n // Get input types for better email detection\n const inputs = form.querySelectorAll(\"input, select, textarea\")\n for (const input of inputs) {\n const name = input.getAttribute(\"name\")\n if (name && input instanceof HTMLInputElement) {\n inputTypes.set(name, input.type)\n }\n }\n\n formData.forEach((value, key) => {\n // Only capture string values, skip files\n if (typeof value === \"string\") {\n fields[key] = value\n }\n })\n\n // Sanitize fields to remove sensitive data\n const sanitizedFields = sanitizeFormFields(fields, formDenylist)\n\n // Auto-identify if callback is set and we find identity fields\n // Use unsanitized fields for identity extraction (email might be in there)\n if (identityCallback) {\n const identity = extractIdentityFromForm(fields, inputTypes)\n if (identity) {\n identityCallback(identity)\n }\n }\n\n // Emit form event (with sanitized fields)\n if (sanitizedFields && Object.keys(sanitizedFields).length > 0) {\n formCallback(url, formId, sanitizedFields)\n }\n}\n\n// ============================================\n// CLEANUP\n// ============================================\n\n/**\n * Stop all autocapture tracking.\n */\nexport function stopAutocapture(): void {\n pageviewCallback = null\n formCallback = null\n identityCallback = null\n document.removeEventListener(\"submit\", handleFormSubmit, true)\n}\n","// ============================================\n// VISITOR ID STORAGE\n// ============================================\n\nconst VISITOR_ID_KEY = \"outlit_visitor_id\"\n\n/**\n * Generate a UUID v4.\n * Uses crypto.randomUUID if available, otherwise falls back to manual generation.\n */\nexport function generateVisitorId(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n\n // Fallback for older browsers\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === \"x\" ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\n/**\n * Get the visitor ID from storage, generating a new one if needed.\n * Tries localStorage first, falls back to cookie.\n */\nexport function getOrCreateVisitorId(): string {\n // Try localStorage first\n try {\n const stored = localStorage.getItem(VISITOR_ID_KEY)\n if (stored && isValidUuid(stored)) {\n return stored\n }\n } catch {\n // localStorage not available\n }\n\n // Try cookie fallback\n const cookieValue = getCookie(VISITOR_ID_KEY)\n if (cookieValue && isValidUuid(cookieValue)) {\n // Also store in localStorage for consistency\n try {\n localStorage.setItem(VISITOR_ID_KEY, cookieValue)\n } catch {\n // Ignore\n }\n return cookieValue\n }\n\n // Generate new visitor ID\n const visitorId = generateVisitorId()\n persistVisitorId(visitorId)\n return visitorId\n}\n\n/**\n * Persist visitor ID to both localStorage and cookie.\n */\nfunction persistVisitorId(visitorId: string): void {\n // Store in localStorage\n try {\n localStorage.setItem(VISITOR_ID_KEY, visitorId)\n } catch {\n // localStorage not available\n }\n\n // Also store in cookie for cross-subdomain support\n setCookie(VISITOR_ID_KEY, visitorId, 365) // 1 year\n}\n\n/**\n * Basic UUID validation.\n */\nfunction isValidUuid(value: string): boolean {\n return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)\n}\n\n// ============================================\n// COOKIE HELPERS\n// ============================================\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") return null\n\n const value = `; ${document.cookie}`\n const parts = value.split(`; ${name}=`)\n if (parts.length === 2) {\n return parts.pop()?.split(\";\").shift() ?? null\n }\n return null\n}\n\n/**\n * Get the root domain for cross-subdomain cookie sharing.\n * e.g., \"www.example.com\" → \"example.com\"\n * \"app.staging.example.com\" → \"example.com\"\n * \"localhost\" → null (no domain attribute needed)\n */\nfunction getRootDomain(): string | null {\n if (typeof window === \"undefined\") return null\n\n const hostname = window.location.hostname\n\n // Don't set domain for localhost or IP addresses\n if (hostname === \"localhost\" || /^(\\d{1,3}\\.){3}\\d{1,3}$/.test(hostname)) {\n return null\n }\n\n // Split hostname into parts\n const parts = hostname.split(\".\")\n\n // For simple domains like \"example.com\", return \".example.com\"\n // For subdomains like \"www.example.com\" or \"app.example.com\", return \".example.com\"\n if (parts.length >= 2) {\n // Handle common TLDs with two parts (e.g., .co.uk, .com.au)\n const twoPartTlds = [\"co.uk\", \"com.au\", \"co.nz\", \"org.uk\", \"net.au\", \"com.br\"]\n const lastTwo = parts.slice(-2).join(\".\")\n\n if (twoPartTlds.includes(lastTwo) && parts.length >= 3) {\n // e.g., \"www.example.co.uk\" → \"example.co.uk\"\n return parts.slice(-3).join(\".\")\n }\n\n // Standard case: \"www.example.com\" → \"example.com\"\n return parts.slice(-2).join(\".\")\n }\n\n return null\n}\n\nfunction setCookie(name: string, value: string, days: number): void {\n if (typeof document === \"undefined\") return\n\n const expires = new Date()\n expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)\n\n // Build cookie string\n let cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`\n\n // Add domain for cross-subdomain support\n const rootDomain = getRootDomain()\n if (rootDomain) {\n cookie += `;domain=${rootDomain}`\n }\n\n document.cookie = cookie\n}\n","import type { BrowserIdentifyOptions, BrowserTrackOptions } from \"@outlit/core\"\nimport { useCallback, useContext } from \"react\"\nimport { OutlitContext } from \"./provider\"\n\n// ============================================\n// useOutlit Hook\n// ============================================\n\nexport interface UseOutlitReturn {\n /**\n * Track a custom event.\n */\n track: (eventName: string, properties?: BrowserTrackOptions[\"properties\"]) => void\n\n /**\n * Identify the current visitor.\n * Links the anonymous visitor to a known user.\n */\n identify: (options: BrowserIdentifyOptions) => void\n\n /**\n * Get the current visitor ID.\n * Returns null if tracking is not enabled.\n */\n getVisitorId: () => string | null\n\n /**\n * Whether the tracker is initialized.\n */\n isInitialized: boolean\n\n /**\n * Whether tracking is currently enabled.\n * Will be false if autoTrack is false and enableTracking() hasn't been called.\n */\n isTrackingEnabled: boolean\n\n /**\n * Enable tracking. Call this after obtaining user consent.\n * Only needed if you initialized with autoTrack: false.\n */\n enableTracking: () => void\n}\n\n/**\n * Hook to access the Outlit tracker.\n *\n * @example\n * ```tsx\n * import { useOutlit } from '@outlit/tracker/react'\n *\n * function MyComponent() {\n * const { track, identify } = useOutlit()\n *\n * return (\n * <button onClick={() => track('button_clicked', { id: 'cta' })}>\n * Click me\n * </button>\n * )\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With consent management\n * function ConsentBanner() {\n * const { enableTracking, isTrackingEnabled } = useOutlit()\n *\n * if (isTrackingEnabled) return null\n *\n * return (\n * <div>\n * <p>We use cookies to improve your experience.</p>\n * <button onClick={enableTracking}>Accept</button>\n * </div>\n * )\n * }\n * ```\n */\nexport function useOutlit(): UseOutlitReturn {\n const { tracker, isInitialized, isTrackingEnabled, enableTracking } = useContext(OutlitContext)\n\n const track = useCallback(\n (eventName: string, properties?: BrowserTrackOptions[\"properties\"]) => {\n if (!tracker) {\n console.warn(\"[Outlit] Tracker not initialized. Make sure OutlitProvider is mounted.\")\n return\n }\n tracker.track(eventName, properties)\n },\n [tracker],\n )\n\n const identify = useCallback(\n (options: BrowserIdentifyOptions) => {\n if (!tracker) {\n console.warn(\"[Outlit] Tracker not initialized. Make sure OutlitProvider is mounted.\")\n return\n }\n tracker.identify(options)\n },\n [tracker],\n )\n\n const getVisitorId = useCallback(() => {\n if (!tracker) return null\n return tracker.getVisitorId()\n }, [tracker])\n\n return {\n track,\n identify,\n getVisitorId,\n isInitialized,\n isTrackingEnabled,\n enableTracking,\n }\n}\n\n// ============================================\n// useTrack Hook (convenience)\n// ============================================\n\n/**\n * Convenience hook that returns just the track function.\n *\n * @example\n * ```tsx\n * import { useTrack } from '@outlit/tracker/react'\n *\n * function MyComponent() {\n * const track = useTrack()\n * return <button onClick={() => track('clicked')}>Click</button>\n * }\n * ```\n */\nexport function useTrack() {\n const { track } = useOutlit()\n return track\n}\n\n// ============================================\n// useIdentify Hook (convenience)\n// ============================================\n\n/**\n * Convenience hook that returns just the identify function.\n *\n * @example\n * ```tsx\n * import { useIdentify } from '@outlit/tracker/react'\n *\n * function LoginForm() {\n * const identify = useIdentify()\n *\n * const onLogin = (user) => {\n * identify({ email: user.email, traits: { name: user.name } })\n * }\n * }\n * ```\n */\nexport function useIdentify() {\n const { identify } = useOutlit()\n return identify\n}\n"],"mappings":"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","names":["identityCallback","useCallback","useCallback"]}
@@ -0,0 +1,107 @@
1
+ import { TrackerConfig, BrowserTrackOptions, BrowserIdentifyOptions } from '@outlit/core';
2
+
3
+ interface TrackerOptions extends TrackerConfig {
4
+ /**
5
+ * Automatically start tracking on init.
6
+ * Set to false if you need to wait for user consent before tracking.
7
+ * Call enableTracking() to start tracking after consent is obtained.
8
+ * @default true
9
+ */
10
+ autoTrack?: boolean;
11
+ trackPageviews?: boolean;
12
+ trackForms?: boolean;
13
+ formFieldDenylist?: string[];
14
+ flushInterval?: number;
15
+ /**
16
+ * Automatically identify users when they submit forms with email fields.
17
+ * Extracts email and name (first/last) from form fields using heuristics.
18
+ * @default true
19
+ */
20
+ autoIdentify?: boolean;
21
+ }
22
+ declare class Tracker {
23
+ private publicKey;
24
+ private apiHost;
25
+ private visitorId;
26
+ private eventQueue;
27
+ private flushTimer;
28
+ private flushInterval;
29
+ private isInitialized;
30
+ private isTrackingEnabled;
31
+ private options;
32
+ constructor(options: TrackerOptions);
33
+ /**
34
+ * Enable tracking. Call this after obtaining user consent.
35
+ * This will:
36
+ * - Generate/retrieve the visitor ID
37
+ * - Start automatic pageview and form tracking (if configured)
38
+ * - Begin sending events to the server
39
+ *
40
+ * If autoTrack is true (default), this is called automatically on init.
41
+ */
42
+ enableTracking(): void;
43
+ /**
44
+ * Check if tracking is currently enabled.
45
+ */
46
+ isEnabled(): boolean;
47
+ /**
48
+ * Track a custom event.
49
+ */
50
+ track(eventName: string, properties?: BrowserTrackOptions["properties"]): void;
51
+ /**
52
+ * Identify the current visitor.
53
+ * Links the anonymous visitor to a known user.
54
+ */
55
+ identify(options: BrowserIdentifyOptions): void;
56
+ /**
57
+ * Get the current visitor ID.
58
+ * Returns null if tracking is not enabled.
59
+ */
60
+ getVisitorId(): string | null;
61
+ /**
62
+ * Manually flush the event queue.
63
+ */
64
+ flush(): Promise<void>;
65
+ /**
66
+ * Shutdown the tracker.
67
+ */
68
+ shutdown(): Promise<void>;
69
+ private initPageviewTracking;
70
+ private initFormTracking;
71
+ private enqueue;
72
+ private startFlushTimer;
73
+ private sendEvents;
74
+ }
75
+ /**
76
+ * Initialize the Outlit tracker.
77
+ * Should be called once at app startup.
78
+ */
79
+ declare function init(options: TrackerOptions): Tracker;
80
+ /**
81
+ * Get the tracker instance.
82
+ * Throws if not initialized.
83
+ */
84
+ declare function getInstance(): Tracker;
85
+ /**
86
+ * Track a custom event.
87
+ * Convenience method that uses the singleton instance.
88
+ */
89
+ declare function track(eventName: string, properties?: BrowserTrackOptions["properties"]): void;
90
+ /**
91
+ * Identify the current visitor.
92
+ * Convenience method that uses the singleton instance.
93
+ */
94
+ declare function identify(options: BrowserIdentifyOptions): void;
95
+ /**
96
+ * Enable tracking after consent is obtained.
97
+ * Call this in your consent management tool's callback.
98
+ * Convenience method that uses the singleton instance.
99
+ */
100
+ declare function enableTracking(): void;
101
+ /**
102
+ * Check if tracking is currently enabled.
103
+ * Convenience method that uses the singleton instance.
104
+ */
105
+ declare function isTrackingEnabled(): boolean;
106
+
107
+ export { Tracker as T, identify as a, isTrackingEnabled as b, type TrackerOptions as c, enableTracking as e, getInstance as g, init as i, track as t };