@loamly/tracker 2.1.0 → 2.4.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.
- package/README.md +4 -3
- package/dist/index.cjs +1580 -1286
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.mjs +1580 -1286
- package/dist/index.mjs.map +1 -1
- package/dist/loamly.iife.global.js +1578 -1270
- package/dist/loamly.iife.global.js.map +1 -1
- package/dist/loamly.iife.min.global.js +1 -1
- package/dist/loamly.iife.min.global.js.map +1 -1
- package/package.json +1 -1
- package/src/behavioral/form-tracker.ts +107 -1
- package/src/browser.ts +25 -6
- package/src/config.ts +1 -1
- package/src/core.ts +386 -132
- package/src/index.ts +1 -1
- package/src/infrastructure/event-queue.ts +58 -32
- package/src/infrastructure/ping.ts +19 -3
- package/src/types.ts +23 -0
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface QueuedEvent {
|
|
|
16
16
|
id: string
|
|
17
17
|
type: string
|
|
18
18
|
payload: Record<string, unknown>
|
|
19
|
+
headers?: Record<string, string>
|
|
19
20
|
timestamp: number
|
|
20
21
|
retries: number
|
|
21
22
|
}
|
|
@@ -26,6 +27,7 @@ interface EventQueueConfig {
|
|
|
26
27
|
maxRetries: number
|
|
27
28
|
retryDelayMs: number
|
|
28
29
|
storageKey: string
|
|
30
|
+
apiKey?: string
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
const DEFAULT_QUEUE_CONFIG: EventQueueConfig = {
|
|
@@ -52,11 +54,12 @@ export class EventQueue {
|
|
|
52
54
|
/**
|
|
53
55
|
* Add event to queue
|
|
54
56
|
*/
|
|
55
|
-
push(type: string, payload: Record<string, unknown>): void {
|
|
57
|
+
push(type: string, payload: Record<string, unknown>, headers?: Record<string, string>): void {
|
|
56
58
|
const event: QueuedEvent = {
|
|
57
59
|
id: this.generateId(),
|
|
58
60
|
type,
|
|
59
61
|
payload,
|
|
62
|
+
headers,
|
|
60
63
|
timestamp: Date.now(),
|
|
61
64
|
retries: 0,
|
|
62
65
|
}
|
|
@@ -92,24 +95,31 @@ export class EventQueue {
|
|
|
92
95
|
flushBeacon(): boolean {
|
|
93
96
|
if (this.queue.length === 0) return true
|
|
94
97
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
_queue_id: e.id,
|
|
99
|
-
_queue_timestamp: e.timestamp,
|
|
100
|
-
}))
|
|
98
|
+
const baseUrl = this.config.apiKey
|
|
99
|
+
? `${this.endpoint}?api_key=${encodeURIComponent(this.config.apiKey)}`
|
|
100
|
+
: this.endpoint
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
let allSent = true
|
|
103
|
+
for (const event of this.queue) {
|
|
104
|
+
const payload = {
|
|
105
|
+
...event.payload,
|
|
106
|
+
_queue_id: event.id,
|
|
107
|
+
_queue_timestamp: event.timestamp,
|
|
108
|
+
}
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
const success = navigator.sendBeacon?.(baseUrl, JSON.stringify(payload)) ?? false
|
|
111
|
+
if (!success) {
|
|
112
|
+
allSent = false
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (allSent) {
|
|
108
118
|
this.queue = []
|
|
109
119
|
this.clearStorage()
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
return
|
|
122
|
+
return allSent
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
/**
|
|
@@ -145,28 +155,44 @@ export class EventQueue {
|
|
|
145
155
|
private async sendBatch(events: QueuedEvent[]): Promise<void> {
|
|
146
156
|
if (events.length === 0) return
|
|
147
157
|
|
|
148
|
-
const payload = {
|
|
149
|
-
events: events.map(e => ({
|
|
150
|
-
type: e.type,
|
|
151
|
-
...e.payload,
|
|
152
|
-
_queue_id: e.id,
|
|
153
|
-
_queue_timestamp: e.timestamp,
|
|
154
|
-
})),
|
|
155
|
-
batch: true,
|
|
156
|
-
}
|
|
157
|
-
|
|
158
158
|
try {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
const results = await Promise.allSettled(
|
|
160
|
+
events.map(async (event) => {
|
|
161
|
+
const response = await fetch(this.endpoint, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
...(event.headers || {}),
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
...event.payload,
|
|
169
|
+
_queue_id: event.id,
|
|
170
|
+
_queue_timestamp: event.timestamp,
|
|
171
|
+
}),
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
throw new Error(`HTTP ${response.status}`)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const failedEvents = events.filter((_, index) => results[index]?.status === 'rejected')
|
|
181
|
+
if (failedEvents.length > 0) {
|
|
182
|
+
// Retry failed events only
|
|
183
|
+
for (const event of failedEvents) {
|
|
184
|
+
if (event.retries < this.config.maxRetries) {
|
|
185
|
+
event.retries++
|
|
186
|
+
this.queue.push(event)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Schedule retry with exponential backoff
|
|
190
|
+
const delay = this.config.retryDelayMs * Math.pow(2, failedEvents[0].retries - 1)
|
|
191
|
+
setTimeout(() => this.flush(), delay)
|
|
167
192
|
}
|
|
193
|
+
return
|
|
168
194
|
} catch (error) {
|
|
169
|
-
// Retry
|
|
195
|
+
// Retry all events if we hit an unexpected error
|
|
170
196
|
for (const event of events) {
|
|
171
197
|
if (event.retries < this.config.maxRetries) {
|
|
172
198
|
event.retries++
|
|
@@ -32,6 +32,7 @@ export class PingService {
|
|
|
32
32
|
private config: PingConfig
|
|
33
33
|
private pageLoadTime: number
|
|
34
34
|
private isVisible = true
|
|
35
|
+
private isFocused = true
|
|
35
36
|
private currentScrollDepth = 0
|
|
36
37
|
private sessionId: string
|
|
37
38
|
private visitorId: string
|
|
@@ -47,6 +48,8 @@ export class PingService {
|
|
|
47
48
|
this.visitorId = visitorId
|
|
48
49
|
this.version = version
|
|
49
50
|
this.pageLoadTime = Date.now()
|
|
51
|
+
this.isVisible = document.visibilityState === 'visible'
|
|
52
|
+
this.isFocused = typeof document.hasFocus === 'function' ? document.hasFocus() : true
|
|
50
53
|
this.config = {
|
|
51
54
|
interval: DEFAULT_CONFIG.pingInterval,
|
|
52
55
|
endpoint: '',
|
|
@@ -55,6 +58,8 @@ export class PingService {
|
|
|
55
58
|
|
|
56
59
|
// Track visibility
|
|
57
60
|
document.addEventListener('visibilitychange', this.handleVisibilityChange)
|
|
61
|
+
window.addEventListener('focus', this.handleFocusChange)
|
|
62
|
+
window.addEventListener('blur', this.handleFocusChange)
|
|
58
63
|
|
|
59
64
|
// Track scroll depth
|
|
60
65
|
window.addEventListener('scroll', this.handleScroll, { passive: true })
|
|
@@ -67,13 +72,15 @@ export class PingService {
|
|
|
67
72
|
if (this.intervalId) return
|
|
68
73
|
|
|
69
74
|
this.intervalId = setInterval(() => {
|
|
70
|
-
if (this.isVisible) {
|
|
75
|
+
if (this.isVisible && this.isFocused) {
|
|
71
76
|
this.ping()
|
|
72
77
|
}
|
|
73
78
|
}, this.config.interval)
|
|
74
79
|
|
|
75
80
|
// Send initial ping
|
|
76
|
-
this.
|
|
81
|
+
if (this.isVisible && this.isFocused) {
|
|
82
|
+
this.ping()
|
|
83
|
+
}
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
/**
|
|
@@ -86,6 +93,8 @@ export class PingService {
|
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
document.removeEventListener('visibilitychange', this.handleVisibilityChange)
|
|
96
|
+
window.removeEventListener('focus', this.handleFocusChange)
|
|
97
|
+
window.removeEventListener('blur', this.handleFocusChange)
|
|
89
98
|
window.removeEventListener('scroll', this.handleScroll)
|
|
90
99
|
}
|
|
91
100
|
|
|
@@ -108,7 +117,7 @@ export class PingService {
|
|
|
108
117
|
url: window.location.href,
|
|
109
118
|
time_on_page_ms: Date.now() - this.pageLoadTime,
|
|
110
119
|
scroll_depth: this.currentScrollDepth,
|
|
111
|
-
is_active: this.isVisible,
|
|
120
|
+
is_active: this.isVisible && this.isFocused,
|
|
112
121
|
tracker_version: this.version,
|
|
113
122
|
}
|
|
114
123
|
}
|
|
@@ -137,6 +146,13 @@ export class PingService {
|
|
|
137
146
|
this.isVisible = document.visibilityState === 'visible'
|
|
138
147
|
}
|
|
139
148
|
|
|
149
|
+
private handleFocusChange = (): void => {
|
|
150
|
+
this.isFocused = typeof document.hasFocus === 'function' ? document.hasFocus() : true
|
|
151
|
+
if (this.intervalId && this.isVisible && this.isFocused) {
|
|
152
|
+
this.ping()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
140
156
|
private handleScroll = (): void => {
|
|
141
157
|
const scrollPercent = Math.round(
|
|
142
158
|
((window.scrollY + window.innerHeight) / document.documentElement.scrollHeight) * 100
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
export interface LoamlyConfig {
|
|
7
7
|
/** Your Loamly API key (found in dashboard) */
|
|
8
8
|
apiKey?: string
|
|
9
|
+
|
|
10
|
+
/** Workspace ID (required when using public tracker key) */
|
|
11
|
+
workspaceId?: string
|
|
9
12
|
|
|
10
13
|
/** Custom API host (default: https://app.loamly.ai) */
|
|
11
14
|
apiHost?: string
|
|
@@ -126,6 +129,23 @@ export interface LoamlyTracker {
|
|
|
126
129
|
/** Track a custom event */
|
|
127
130
|
track: (eventName: string, options?: TrackEventOptions) => void
|
|
128
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Track a behavioral event (for secondary modules/plugins)
|
|
134
|
+
*
|
|
135
|
+
* This is the recommended way for secondary tracking modules to send events.
|
|
136
|
+
* It automatically includes workspace_id, visitor_id, session_id, and handles
|
|
137
|
+
* batching, queueing, and retry logic.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```js
|
|
141
|
+
* Loamly.trackBehavioral('outbound_click', {
|
|
142
|
+
* url: 'https://example.com',
|
|
143
|
+
* text: 'Click here'
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
trackBehavioral: (eventType: string, eventData: Record<string, unknown>) => void
|
|
148
|
+
|
|
129
149
|
/** Track a conversion/revenue event */
|
|
130
150
|
conversion: (eventName: string, revenue: number, currency?: string) => void
|
|
131
151
|
|
|
@@ -138,6 +158,9 @@ export interface LoamlyTracker {
|
|
|
138
158
|
/** Get the current visitor ID */
|
|
139
159
|
getVisitorId: () => string | null
|
|
140
160
|
|
|
161
|
+
/** Get the current workspace ID */
|
|
162
|
+
getWorkspaceId: () => string | null
|
|
163
|
+
|
|
141
164
|
/** Get AI detection result for current page */
|
|
142
165
|
getAIDetection: () => AIDetectionResult | null
|
|
143
166
|
|