@agentuity/frontend 0.0.111 → 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.
Files changed (81) hide show
  1. package/dist/analytics/beacon.d.ts +15 -0
  2. package/dist/analytics/beacon.d.ts.map +1 -0
  3. package/dist/analytics/beacon.js +177 -0
  4. package/dist/analytics/beacon.js.map +1 -0
  5. package/dist/analytics/collectors/clicks.d.ts +10 -0
  6. package/dist/analytics/collectors/clicks.d.ts.map +1 -0
  7. package/dist/analytics/collectors/clicks.js +84 -0
  8. package/dist/analytics/collectors/clicks.js.map +1 -0
  9. package/dist/analytics/collectors/errors.d.ts +5 -0
  10. package/dist/analytics/collectors/errors.d.ts.map +1 -0
  11. package/dist/analytics/collectors/errors.js +43 -0
  12. package/dist/analytics/collectors/errors.js.map +1 -0
  13. package/dist/analytics/collectors/forms.d.ts +5 -0
  14. package/dist/analytics/collectors/forms.d.ts.map +1 -0
  15. package/dist/analytics/collectors/forms.js +55 -0
  16. package/dist/analytics/collectors/forms.js.map +1 -0
  17. package/dist/analytics/collectors/pageview.d.ts +15 -0
  18. package/dist/analytics/collectors/pageview.d.ts.map +1 -0
  19. package/dist/analytics/collectors/pageview.js +64 -0
  20. package/dist/analytics/collectors/pageview.js.map +1 -0
  21. package/dist/analytics/collectors/scroll.d.ts +17 -0
  22. package/dist/analytics/collectors/scroll.d.ts.map +1 -0
  23. package/dist/analytics/collectors/scroll.js +93 -0
  24. package/dist/analytics/collectors/scroll.js.map +1 -0
  25. package/dist/analytics/collectors/spa.d.ts +10 -0
  26. package/dist/analytics/collectors/spa.d.ts.map +1 -0
  27. package/dist/analytics/collectors/spa.js +53 -0
  28. package/dist/analytics/collectors/spa.js.map +1 -0
  29. package/dist/analytics/collectors/visibility.d.ts +18 -0
  30. package/dist/analytics/collectors/visibility.d.ts.map +1 -0
  31. package/dist/analytics/collectors/visibility.js +81 -0
  32. package/dist/analytics/collectors/visibility.js.map +1 -0
  33. package/dist/analytics/collectors/webvitals.d.ts +6 -0
  34. package/dist/analytics/collectors/webvitals.d.ts.map +1 -0
  35. package/dist/analytics/collectors/webvitals.js +111 -0
  36. package/dist/analytics/collectors/webvitals.js.map +1 -0
  37. package/dist/analytics/events.d.ts +18 -0
  38. package/dist/analytics/events.d.ts.map +1 -0
  39. package/dist/analytics/events.js +126 -0
  40. package/dist/analytics/events.js.map +1 -0
  41. package/dist/analytics/index.d.ts +12 -0
  42. package/dist/analytics/index.d.ts.map +1 -0
  43. package/dist/analytics/index.js +12 -0
  44. package/dist/analytics/index.js.map +1 -0
  45. package/dist/analytics/offline.d.ts +19 -0
  46. package/dist/analytics/offline.d.ts.map +1 -0
  47. package/dist/analytics/offline.js +145 -0
  48. package/dist/analytics/offline.js.map +1 -0
  49. package/dist/analytics/types.d.ts +113 -0
  50. package/dist/analytics/types.d.ts.map +1 -0
  51. package/dist/analytics/types.js +2 -0
  52. package/dist/analytics/types.js.map +1 -0
  53. package/dist/analytics/utils/storage.d.ts +13 -0
  54. package/dist/analytics/utils/storage.d.ts.map +1 -0
  55. package/dist/analytics/utils/storage.js +63 -0
  56. package/dist/analytics/utils/storage.js.map +1 -0
  57. package/dist/analytics/utils/utm.d.ts +12 -0
  58. package/dist/analytics/utils/utm.d.ts.map +1 -0
  59. package/dist/analytics/utils/utm.js +27 -0
  60. package/dist/analytics/utils/utm.js.map +1 -0
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +2 -0
  64. package/dist/index.js.map +1 -1
  65. package/package.json +3 -3
  66. package/src/analytics/beacon.ts +203 -0
  67. package/src/analytics/collectors/clicks.ts +100 -0
  68. package/src/analytics/collectors/errors.ts +49 -0
  69. package/src/analytics/collectors/forms.ts +64 -0
  70. package/src/analytics/collectors/pageview.ts +76 -0
  71. package/src/analytics/collectors/scroll.ts +112 -0
  72. package/src/analytics/collectors/spa.ts +60 -0
  73. package/src/analytics/collectors/visibility.ts +94 -0
  74. package/src/analytics/collectors/webvitals.ts +129 -0
  75. package/src/analytics/events.ts +144 -0
  76. package/src/analytics/index.ts +21 -0
  77. package/src/analytics/offline.ts +163 -0
  78. package/src/analytics/types.ts +139 -0
  79. package/src/analytics/utils/storage.ts +64 -0
  80. package/src/analytics/utils/utm.ts +36 -0
  81. package/src/index.ts +18 -0
@@ -0,0 +1,144 @@
1
+ import type { AnalyticsEvent, AnalyticsBatchPayload, AnalyticsPageConfig } from './types';
2
+ import { getVisitorId } from './utils/storage';
3
+
4
+ const BATCH_SIZE = 10;
5
+ const BATCH_TIMEOUT_MS = 5000;
6
+ const COLLECT_ENDPOINT = '/_agentuity/webanalytics/collect';
7
+
8
+ let eventQueue: AnalyticsEvent[] = [];
9
+ let batchTimeout: ReturnType<typeof setTimeout> | null = null;
10
+ let config: AnalyticsPageConfig | null = null;
11
+
12
+ /**
13
+ * Initialize the event queue with config
14
+ */
15
+ export function initEventQueue(pageConfig: AnalyticsPageConfig): void {
16
+ config = pageConfig;
17
+
18
+ // Flush on page unload
19
+ if (typeof window !== 'undefined') {
20
+ window.addEventListener('visibilitychange', () => {
21
+ if (document.visibilityState === 'hidden') {
22
+ flushEvents();
23
+ }
24
+ });
25
+
26
+ window.addEventListener('pagehide', () => {
27
+ flushEvents();
28
+ });
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Queue an event for sending
34
+ */
35
+ export function queueEvent(event: AnalyticsEvent): void {
36
+ if (!config) {
37
+ return;
38
+ }
39
+
40
+ // Apply sample rate (default: 1 = 100%)
41
+ const sampleRate = config.sampleRate ?? 1;
42
+ if (sampleRate < 1 && Math.random() > sampleRate) {
43
+ return;
44
+ }
45
+
46
+ // Check exclude patterns
47
+ const excludePatterns = config.excludePatterns ?? [];
48
+ if (excludePatterns.length > 0) {
49
+ const currentPath = window.location.pathname;
50
+ for (const pattern of excludePatterns) {
51
+ try {
52
+ if (new RegExp(pattern).test(currentPath)) {
53
+ return;
54
+ }
55
+ } catch {
56
+ // Invalid regex, skip
57
+ }
58
+ }
59
+ }
60
+
61
+ // Add global properties to event data
62
+ if (config.globalProperties && Object.keys(config.globalProperties).length > 0) {
63
+ event.event_data = {
64
+ ...config.globalProperties,
65
+ ...event.event_data,
66
+ };
67
+ }
68
+
69
+ eventQueue.push(event);
70
+
71
+ // Flush if batch size reached
72
+ if (eventQueue.length >= BATCH_SIZE) {
73
+ flushEvents();
74
+ } else if (!batchTimeout) {
75
+ // Set timeout for batch flush
76
+ batchTimeout = setTimeout(() => {
77
+ flushEvents();
78
+ }, BATCH_TIMEOUT_MS);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Flush all queued events
84
+ */
85
+ export function flushEvents(): void {
86
+ if (batchTimeout) {
87
+ clearTimeout(batchTimeout);
88
+ batchTimeout = null;
89
+ }
90
+
91
+ if (eventQueue.length === 0 || !config) {
92
+ return;
93
+ }
94
+
95
+ const events = eventQueue;
96
+ eventQueue = [];
97
+
98
+ // In dev mode, log to console instead of sending
99
+ if (config.isDevmode) {
100
+ console.debug('[Agentuity Analytics] Events:', events);
101
+ return;
102
+ }
103
+
104
+ const payload: AnalyticsBatchPayload = {
105
+ org_id: config.orgId,
106
+ project_id: config.projectId,
107
+ session_id: config.sessionId,
108
+ thread_id: config.threadId,
109
+ visitor_id: getVisitorId(),
110
+ is_devmode: config.isDevmode,
111
+ events,
112
+ };
113
+
114
+ const body = JSON.stringify(payload);
115
+
116
+ // Use sendBeacon for reliable delivery
117
+ if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
118
+ const sent = navigator.sendBeacon(COLLECT_ENDPOINT, body);
119
+ if (sent) {
120
+ return;
121
+ }
122
+ }
123
+
124
+ // Fallback to fetch with keepalive
125
+ if (typeof fetch !== 'undefined') {
126
+ fetch(COLLECT_ENDPOINT, {
127
+ method: 'POST',
128
+ headers: {
129
+ 'Content-Type': 'application/json',
130
+ },
131
+ body,
132
+ keepalive: true,
133
+ }).catch(() => {
134
+ // Silent failure - analytics is best effort
135
+ });
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Get current queue length (for testing)
141
+ */
142
+ export function getQueueLength(): number {
143
+ return eventQueue.length;
144
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Web Analytics for Agentuity SDK applications
3
+ *
4
+ * This module provides client-side analytics tracking for web applications
5
+ * built with the Agentuity SDK.
6
+ */
7
+
8
+ export { getAnalytics, track, initBeacon } from './beacon';
9
+
10
+ export type {
11
+ AnalyticsClient,
12
+ AnalyticsEvent,
13
+ AnalyticsEventType,
14
+ AnalyticsBatchPayload,
15
+ AnalyticsPageConfig,
16
+ } from './types';
17
+
18
+ // Re-export utilities for advanced usage
19
+ export { trackPageview, createBaseEvent } from './collectors/pageview';
20
+ export { getVisitorId, isOptedOut, setOptOut } from './utils/storage';
21
+ export { getUTMParams } from './utils/utm';
@@ -0,0 +1,163 @@
1
+ import type { AnalyticsEvent } from './types';
2
+
3
+ const DB_NAME = 'agentuity_analytics';
4
+ const STORE_NAME = 'events';
5
+ const DB_VERSION = 1;
6
+ const MAX_QUEUE_SIZE = 1000;
7
+
8
+ let db: IDBDatabase | null = null;
9
+ let dbInitPromise: Promise<IDBDatabase | null> | null = null;
10
+
11
+ /**
12
+ * Initialize IndexedDB for offline event storage
13
+ */
14
+ async function initDB(): Promise<IDBDatabase | null> {
15
+ if (typeof indexedDB === 'undefined') {
16
+ return null;
17
+ }
18
+
19
+ return new Promise((resolve) => {
20
+ try {
21
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
22
+
23
+ request.onerror = () => {
24
+ resolve(null);
25
+ };
26
+
27
+ request.onsuccess = () => {
28
+ resolve(request.result);
29
+ };
30
+
31
+ request.onupgradeneeded = (e) => {
32
+ const database = (e.target as IDBOpenDBRequest).result;
33
+ if (!database.objectStoreNames.contains(STORE_NAME)) {
34
+ database.createObjectStore(STORE_NAME, { keyPath: 'id' });
35
+ }
36
+ };
37
+ } catch {
38
+ resolve(null);
39
+ }
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Get database instance
45
+ */
46
+ async function getDB(): Promise<IDBDatabase | null> {
47
+ if (db) {
48
+ return db;
49
+ }
50
+
51
+ if (!dbInitPromise) {
52
+ dbInitPromise = initDB();
53
+ }
54
+
55
+ db = await dbInitPromise;
56
+ return db;
57
+ }
58
+
59
+ /**
60
+ * Store event in IndexedDB for offline persistence
61
+ */
62
+ export async function storeOfflineEvent(event: AnalyticsEvent): Promise<void> {
63
+ const database = await getDB();
64
+ if (!database) {
65
+ return;
66
+ }
67
+
68
+ try {
69
+ const transaction = database.transaction(STORE_NAME, 'readwrite');
70
+ const store = transaction.objectStore(STORE_NAME);
71
+
72
+ // Check current count and evict old events if needed before adding
73
+ const count = await new Promise<number>((resolve) => {
74
+ const countRequest = store.count();
75
+ countRequest.onsuccess = () => resolve(countRequest.result);
76
+ countRequest.onerror = () => resolve(0);
77
+ });
78
+
79
+ if (count >= MAX_QUEUE_SIZE) {
80
+ // Evict oldest event (FIFO) before adding new one
81
+ await new Promise<void>((resolve) => {
82
+ const cursorRequest = store.openCursor();
83
+ cursorRequest.onsuccess = () => {
84
+ const cursor = cursorRequest.result;
85
+ if (cursor) {
86
+ const deleteRequest = cursor.delete();
87
+ deleteRequest.onsuccess = () => resolve();
88
+ deleteRequest.onerror = () => resolve();
89
+ } else {
90
+ resolve();
91
+ }
92
+ };
93
+ cursorRequest.onerror = () => resolve();
94
+ });
95
+ }
96
+
97
+ store.add(event);
98
+ } catch {
99
+ // Silent failure
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get all offline events and clear them
105
+ */
106
+ export async function getAndClearOfflineEvents(): Promise<AnalyticsEvent[]> {
107
+ const database = await getDB();
108
+ if (!database) {
109
+ return [];
110
+ }
111
+
112
+ return new Promise((resolve) => {
113
+ try {
114
+ const transaction = database.transaction(STORE_NAME, 'readwrite');
115
+ const store = transaction.objectStore(STORE_NAME);
116
+
117
+ const events: AnalyticsEvent[] = [];
118
+ const request = store.openCursor();
119
+
120
+ request.onsuccess = () => {
121
+ const cursor = request.result;
122
+ if (cursor) {
123
+ events.push(cursor.value as AnalyticsEvent);
124
+ cursor.delete();
125
+ cursor.continue();
126
+ } else {
127
+ resolve(events);
128
+ }
129
+ };
130
+
131
+ request.onerror = () => {
132
+ resolve([]);
133
+ };
134
+ } catch {
135
+ resolve([]);
136
+ }
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Check if we're online
142
+ */
143
+ export function isOnline(): boolean {
144
+ if (typeof navigator === 'undefined') {
145
+ return true;
146
+ }
147
+ return navigator.onLine !== false;
148
+ }
149
+
150
+ /**
151
+ * Initialize offline support
152
+ * Listens for online event to flush queued events
153
+ */
154
+ export function initOfflineSupport(flushCallback: () => void): void {
155
+ if (typeof window === 'undefined') {
156
+ return;
157
+ }
158
+
159
+ window.addEventListener('online', () => {
160
+ // Flush offline events when coming back online
161
+ flushCallback();
162
+ });
163
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Analytics event types
3
+ */
4
+ export type AnalyticsEventType =
5
+ | 'pageview'
6
+ | 'click'
7
+ | 'scroll'
8
+ | 'visibility'
9
+ | 'error'
10
+ | 'custom'
11
+ | 'web_vital'
12
+ | 'form_submit'
13
+ | 'outbound_link';
14
+
15
+ /**
16
+ * Analytics event sent to the collection endpoint
17
+ */
18
+ export interface AnalyticsEvent {
19
+ id: string;
20
+ timestamp: number;
21
+ timezone_offset: number;
22
+
23
+ event_type: AnalyticsEventType;
24
+ event_name?: string;
25
+ event_data?: Record<string, unknown>;
26
+
27
+ url: string;
28
+ path: string;
29
+ referrer: string;
30
+ title: string;
31
+
32
+ screen_width: number;
33
+ screen_height: number;
34
+ viewport_width: number;
35
+ viewport_height: number;
36
+ device_pixel_ratio: number;
37
+ user_agent: string;
38
+ language: string;
39
+
40
+ load_time?: number;
41
+ dom_ready?: number;
42
+ ttfb?: number;
43
+ fcp?: number;
44
+ lcp?: number;
45
+ cls?: number;
46
+ inp?: number;
47
+
48
+ scroll_depth?: number;
49
+ time_on_page?: number;
50
+
51
+ utm_source?: string;
52
+ utm_medium?: string;
53
+ utm_campaign?: string;
54
+ utm_term?: string;
55
+ utm_content?: string;
56
+ }
57
+
58
+ /**
59
+ * Batch payload sent to /_agentuity/webanalytics/collect
60
+ */
61
+ export interface AnalyticsBatchPayload {
62
+ org_id: string;
63
+ project_id: string;
64
+ session_id: string;
65
+ thread_id: string;
66
+ visitor_id: string;
67
+ is_devmode: boolean;
68
+ events: AnalyticsEvent[];
69
+ }
70
+
71
+ /**
72
+ * Configuration injected by SDK runtime into window.__AGENTUITY_ANALYTICS__
73
+ */
74
+ export interface AnalyticsPageConfig {
75
+ enabled: boolean;
76
+ orgId: string;
77
+ projectId: string;
78
+ sessionId: string;
79
+ threadId: string;
80
+ isDevmode: boolean;
81
+
82
+ trackClicks?: boolean;
83
+ trackScroll?: boolean;
84
+ trackOutboundLinks?: boolean;
85
+ trackForms?: boolean;
86
+ trackWebVitals?: boolean;
87
+ trackErrors?: boolean;
88
+ trackSPANavigation?: boolean;
89
+ requireConsent?: boolean;
90
+ sampleRate?: number;
91
+ excludePatterns?: string[];
92
+ globalProperties?: Record<string, unknown>;
93
+ }
94
+
95
+ /**
96
+ * Public analytics client interface
97
+ */
98
+ export interface AnalyticsClient {
99
+ /**
100
+ * Track a custom event
101
+ */
102
+ track(eventName: string, properties?: Record<string, unknown>): void;
103
+
104
+ /**
105
+ * Identify the current user (sets visitor properties)
106
+ */
107
+ identify(userId: string, traits?: Record<string, unknown>): void;
108
+
109
+ /**
110
+ * Manually track a page view
111
+ */
112
+ pageview(path?: string): void;
113
+
114
+ /**
115
+ * Flush pending events immediately
116
+ */
117
+ flush(): Promise<void>;
118
+
119
+ /**
120
+ * Opt out of analytics
121
+ */
122
+ optOut(): void;
123
+
124
+ /**
125
+ * Opt back in to analytics
126
+ */
127
+ optIn(): void;
128
+
129
+ /**
130
+ * Check if analytics is currently enabled
131
+ */
132
+ isEnabled(): boolean;
133
+ }
134
+
135
+ declare global {
136
+ interface Window {
137
+ __AGENTUITY_ANALYTICS__?: AnalyticsPageConfig;
138
+ }
139
+ }
@@ -0,0 +1,64 @@
1
+ const VISITOR_ID_KEY = 'agentuity_visitor_id';
2
+ const OPT_OUT_KEY = 'agentuity_analytics_optout';
3
+
4
+ /**
5
+ * Generate a random UUID v4
6
+ */
7
+ function generateUUID(): string {
8
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
9
+ return crypto.randomUUID();
10
+ }
11
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
12
+ const r = (Math.random() * 16) | 0;
13
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
14
+ return v.toString(16);
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Get or create the visitor ID from localStorage
20
+ */
21
+ export function getVisitorId(): string {
22
+ if (typeof localStorage === 'undefined') {
23
+ return generateUUID();
24
+ }
25
+
26
+ let visitorId = localStorage.getItem(VISITOR_ID_KEY);
27
+ if (!visitorId) {
28
+ visitorId = `vid_${generateUUID()}`;
29
+ try {
30
+ localStorage.setItem(VISITOR_ID_KEY, visitorId);
31
+ } catch {
32
+ // localStorage might be full or disabled
33
+ }
34
+ }
35
+ return visitorId;
36
+ }
37
+
38
+ /**
39
+ * Check if user has opted out
40
+ */
41
+ export function isOptedOut(): boolean {
42
+ if (typeof localStorage === 'undefined') {
43
+ return false;
44
+ }
45
+ return localStorage.getItem(OPT_OUT_KEY) === 'true';
46
+ }
47
+
48
+ /**
49
+ * Set opt-out status
50
+ */
51
+ export function setOptOut(optOut: boolean): void {
52
+ if (typeof localStorage === 'undefined') {
53
+ return;
54
+ }
55
+ try {
56
+ if (optOut) {
57
+ localStorage.setItem(OPT_OUT_KEY, 'true');
58
+ } else {
59
+ localStorage.removeItem(OPT_OUT_KEY);
60
+ }
61
+ } catch {
62
+ // localStorage might be full or disabled
63
+ }
64
+ }
@@ -0,0 +1,36 @@
1
+ export interface UTMParams {
2
+ utm_source?: string;
3
+ utm_medium?: string;
4
+ utm_campaign?: string;
5
+ utm_term?: string;
6
+ utm_content?: string;
7
+ }
8
+
9
+ /**
10
+ * Extract UTM parameters from the current URL
11
+ */
12
+ export function getUTMParams(): UTMParams {
13
+ if (typeof window === 'undefined') {
14
+ return {};
15
+ }
16
+
17
+ const params = new URLSearchParams(window.location.search);
18
+ const utm: UTMParams = {};
19
+
20
+ const source = params.get('utm_source');
21
+ if (source) utm.utm_source = source;
22
+
23
+ const medium = params.get('utm_medium');
24
+ if (medium) utm.utm_medium = medium;
25
+
26
+ const campaign = params.get('utm_campaign');
27
+ if (campaign) utm.utm_campaign = campaign;
28
+
29
+ const term = params.get('utm_term');
30
+ if (term) utm.utm_term = term;
31
+
32
+ const content = params.get('utm_content');
33
+ if (content) utm.utm_content = content;
34
+
35
+ return utm;
36
+ }
package/src/index.ts CHANGED
@@ -30,3 +30,21 @@ export type {
30
30
  StreamClient,
31
31
  EventHandler,
32
32
  } from './client/types';
33
+
34
+ // Export analytics
35
+ export {
36
+ getAnalytics,
37
+ track,
38
+ initBeacon,
39
+ trackPageview,
40
+ createBaseEvent,
41
+ getVisitorId,
42
+ isOptedOut,
43
+ setOptOut,
44
+ getUTMParams,
45
+ type AnalyticsClient,
46
+ type AnalyticsEvent,
47
+ type AnalyticsEventType,
48
+ type AnalyticsBatchPayload,
49
+ type AnalyticsPageConfig,
50
+ } from './analytics';