@usereq/widget 0.2.3 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usereq/widget",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -19,7 +19,6 @@ import { widgetTriggerRuleKey } from "../shared/widget-config";
19
19
  import { WidgetRuntime, type WidgetRuntimeStatus } from "../renderer/index";
20
20
  import { buildAssistantMessageRevealSteps } from "../chat-widget";
21
21
  import { DEFAULT_STOP_CONVERSATION_PROMPT } from "../shared/stop-confirmation";
22
- import type { WidgetTriggerRuleType } from "../shared/analytics";
23
22
  import {
24
23
  bootstrapWidget,
25
24
  resetWidgetSession,
@@ -27,10 +26,6 @@ import {
27
26
  startWidgetConversation,
28
27
  submitWidgetMessage,
29
28
  } from "../runtime/bootstrap";
30
- import {
31
- createWidgetAnalyticsTracker,
32
- type WidgetAnalyticsTracker,
33
- } from "../runtime/analytics";
34
29
  import { getWidgetShadowCss } from "../shared/shadow-css";
35
30
  import { WIDGET_SHADOW_THEME_CSS } from "../shared/shadow-theme";
36
31
 
@@ -72,8 +67,6 @@ export class AgentWidgetElement extends HTMLElement {
72
67
  private sendVersion = 0;
73
68
  private confirmationVersion = 0;
74
69
  private bootstrapPhase: "idle" | "booting" | "ready" | "blocked" = "idle";
75
- private analyticsTrackerKey = "";
76
- private analyticsTracker: WidgetAnalyticsTracker | null = null;
77
70
  private autoStartTriggered = false;
78
71
 
79
72
  constructor() {
@@ -110,7 +103,6 @@ export class AgentWidgetElement extends HTMLElement {
110
103
 
111
104
  connectedCallback() {
112
105
  this.syncAttributes();
113
- this.recreateAnalyticsTracker();
114
106
  this.renderWidget();
115
107
  if (this.agentId) {
116
108
  void this.ensureBootstrap();
@@ -119,9 +111,6 @@ export class AgentWidgetElement extends HTMLElement {
119
111
 
120
112
  disconnectedCallback() {
121
113
  this.bootstrapPromise = null;
122
- this.analyticsTracker?.dispose();
123
- this.analyticsTracker = null;
124
- this.analyticsTrackerKey = "";
125
114
  this.root.unmount();
126
115
  }
127
116
 
@@ -131,7 +120,6 @@ export class AgentWidgetElement extends HTMLElement {
131
120
  newValue?: string | null,
132
121
  ) {
133
122
  this.syncAttributes();
134
- this.recreateAnalyticsTracker();
135
123
  if (name === "agent-id" && oldValue !== newValue) {
136
124
  this.clearAgentState();
137
125
  }
@@ -217,22 +205,6 @@ export class AgentWidgetElement extends HTMLElement {
217
205
  this.status = "idle";
218
206
  }
219
207
 
220
- private recreateAnalyticsTracker() {
221
- const nextKey = `${this.mode}:${this.agentId}`;
222
- if (nextKey === this.analyticsTrackerKey) {
223
- return;
224
- }
225
-
226
- this.analyticsTracker?.dispose();
227
- this.analyticsTracker = createWidgetAnalyticsTracker({
228
- mode: this.mode,
229
- agentId: this.agentId,
230
- getSession: () => this.sessionState,
231
- getConversationId: () => this.sessionState?.conversationId,
232
- });
233
- this.analyticsTrackerKey = nextKey;
234
- }
235
-
236
208
  private renderWidget() {
237
209
  if (
238
210
  this.bootstrapPhase !== "ready" ||
@@ -305,10 +277,6 @@ export class AgentWidgetElement extends HTMLElement {
305
277
  onSendMessage: (content: string) => void this.sendMessage(content),
306
278
  onConfirmationDecision: (confirmationId: string, accepted: boolean) =>
307
279
  void this.handleConfirmationDecision(confirmationId, accepted),
308
- onWidgetVisible: (triggerRuleType: WidgetTriggerRuleType | null) =>
309
- this.analyticsTracker?.trackView(triggerRuleType),
310
- onLinkClick: (linkUrl: string) =>
311
- this.analyticsTracker?.trackLinkClicked(linkUrl),
312
280
  portalContainer: this.shadowRootRef,
313
281
  }),
314
282
  );
@@ -355,10 +323,6 @@ export class AgentWidgetElement extends HTMLElement {
355
323
  this.welcomeMessage = state.session.widgetConfig.welcomeMessage;
356
324
  this.status = "ready";
357
325
  this.bootstrapPhase = "ready";
358
- if (this.mode === "embed") {
359
- this.analyticsTracker?.trackScriptLoaded();
360
- this.analyticsTracker?.trackUniqueScriptLoaded();
361
- }
362
326
  this.renderWidget();
363
327
  } catch (error) {
364
328
  if (runVersion !== this.bootstrapVersion) {
@@ -461,12 +425,8 @@ export class AgentWidgetElement extends HTMLElement {
461
425
  }
462
426
 
463
427
  private async handleOpenChange(nextOpen: boolean) {
464
- const wasOpen = this.open;
465
428
  this.open = nextOpen;
466
429
  this.renderWidget();
467
- if (!wasOpen && nextOpen && this.mode === "embed") {
468
- this.analyticsTracker?.trackClick();
469
- }
470
430
 
471
431
  if (this.open && (this.status === "idle" || !this.sessionState)) {
472
432
  await this.ensureBootstrap();
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from "./shared/widget-config";
2
- export * from "./shared/analytics";
3
2
  export * from "./renderer/index";
4
3
  export * from "./types";
5
4
  export * from "./shared/shadow-css";
@@ -27,7 +27,6 @@ import {
27
27
  } from "../shared/stop-confirmation";
28
28
  import { widgetCss } from "../styles/widget.css";
29
29
  import { useWidgetTriggerGate } from "../runtime/trigger-rule";
30
- import type { WidgetTriggerRuleType } from "../shared/analytics";
31
30
 
32
31
  export type WidgetRuntimeMode = "embed" | "preview";
33
32
 
@@ -64,8 +63,6 @@ export type WidgetRuntimeProps = {
64
63
  accepted: boolean,
65
64
  ) => void | Promise<void>;
66
65
  portalContainer?: HTMLElement | DocumentFragment | null;
67
- onWidgetVisible?: (triggerRuleType: WidgetTriggerRuleType | null) => void;
68
- onLinkClick?: (linkUrl: string) => void;
69
66
  onLauncherClick?: () => void;
70
67
  };
71
68
 
@@ -100,21 +97,10 @@ export function WidgetRuntime({
100
97
  onStopConversation,
101
98
  onConfirmationDecision,
102
99
  portalContainer,
103
- onWidgetVisible,
104
- onLinkClick,
105
100
  onLauncherClick,
106
101
  }: WidgetRuntimeProps) {
107
102
  const { ready } = useWidgetTriggerGate(triggerRule);
108
103
  const { horizontal, vertical } = splitWidgetPlacement(placement);
109
- const viewTrackedRef = React.useRef(false);
110
-
111
- React.useEffect(() => {
112
- if (mode !== "embed" || !ready || viewTrackedRef.current) {
113
- return;
114
- }
115
- viewTrackedRef.current = true;
116
- onWidgetVisible?.(triggerRule?.triggerRuleType ?? null);
117
- }, [mode, onWidgetVisible, ready, triggerRule]);
118
104
 
119
105
  const { panelMessages, confirmation } = React.useMemo(
120
106
  () => splitConfirmationFromMessages(messages, onConfirmationDecision),
@@ -219,7 +205,6 @@ export function WidgetRuntime({
219
205
  onStopConversation ? handleStop : undefined
220
206
  }
221
207
  onLauncherClick={onLauncherClick}
222
- onLinkClick={onLinkClick}
223
208
  emptyAction={wrappedEmptyAction}
224
209
  confirmation={confirmation}
225
210
  portalContainer={portalEl}
@@ -1,5 +1,4 @@
1
1
  import type {
2
- WidgetAnalyticsBatchRequest,
3
2
  WidgetConversation,
4
3
  WidgetMessage,
5
4
  WidgetPublicChatSendChatResponse,
@@ -91,33 +90,6 @@ export async function sendPublicChatConfirmation(input: {
91
90
  );
92
91
  }
93
92
 
94
- export async function sendWidgetAnalyticsBatch(
95
- input: WidgetAnalyticsBatchRequest,
96
- ): Promise<void> {
97
- const response = await fetch(
98
- new URL("/api/widget/events/batch", `${getWidgetApiBase()}/`).toString(),
99
- {
100
- method: "POST",
101
- headers: input.sessionToken
102
- ? { Authorization: `Bearer ${input.sessionToken}` }
103
- : undefined,
104
- body: JSON.stringify(input),
105
- credentials: "omit",
106
- keepalive: true,
107
- },
108
- );
109
-
110
- if (!response.ok) {
111
- const text = await response.text();
112
- const payload = text ? JSON.parse(text) : null;
113
- throw new Error(
114
- typeof payload === "object" && payload && "error" in payload
115
- ? String((payload as { error?: string }).error ?? response.statusText)
116
- : response.statusText,
117
- );
118
- }
119
- }
120
-
121
93
  export async function getCurrentConversation(sessionToken: string): Promise<{
122
94
  conversation: WidgetConversation | null;
123
95
  }> {
package/src/types.ts CHANGED
@@ -25,14 +25,6 @@ export {
25
25
  widgetAppearanceToAttributes,
26
26
  type WidgetTriggerRule,
27
27
  } from "./shared/widget-config";
28
- export type {
29
- WidgetAnalyticsBatchEvent,
30
- WidgetAnalyticsBatchRequest,
31
- WidgetAnalyticsBatchResponse,
32
- WidgetAnalyticsEventType,
33
- WidgetClientAnalyticsEventType,
34
- WidgetTriggerRuleType,
35
- } from "./shared/analytics";
36
28
 
37
29
  export type WidgetSessionConfig = {
38
30
  sessionToken: string;
@@ -1,431 +0,0 @@
1
- import type { WidgetSessionState } from "../types";
2
- import type {
3
- WidgetAnalyticsBatchEvent,
4
- WidgetAnalyticsBatchRequest,
5
- WidgetClientAnalyticsEventType,
6
- WidgetTriggerRuleType,
7
- } from "../shared/analytics";
8
- import { getWidgetApiBase } from "./api-origin";
9
-
10
- const ANALYTICS_ENABLED = false;
11
-
12
- const ANALYTICS_ENDPOINT = "/api/widget/events/batch";
13
- const DEFAULT_BATCH_SIZE = 10;
14
- const DEFAULT_FLUSH_INTERVAL_MS = 2500;
15
- const MAX_FLUSH_INTERVAL_MS = 120_000;
16
- const MAX_QUEUE_SIZE = 200;
17
- const MAX_EVENT_ATTEMPTS = 5;
18
- const MAX_EVENT_AGE_MS = 10 * 60 * 1000;
19
- const UNIQUE_SCRIPT_LOADED_KEY_PREFIX =
20
- "usereq-widget:unique-script-loaded:";
21
-
22
- type QueuedAnalyticsEvent = {
23
- event: WidgetAnalyticsBatchEvent;
24
- sessionToken: string;
25
- attempts: number;
26
- enqueuedAt: number;
27
- };
28
-
29
- type SendOutcome =
30
- | { outcome: "ok" }
31
- | { outcome: "drop" }
32
- | { outcome: "retry"; retryAfterMs?: number };
33
-
34
- function parseRetryAfter(headerValue: string | null): number | undefined {
35
- if (!headerValue) return undefined;
36
- const trimmed = headerValue.trim();
37
- if (!trimmed) return undefined;
38
- const seconds = Number(trimmed);
39
- if (Number.isFinite(seconds) && seconds >= 0) {
40
- return Math.min(MAX_FLUSH_INTERVAL_MS, seconds * 1000);
41
- }
42
- const dateMs = Date.parse(trimmed);
43
- if (!Number.isNaN(dateMs)) {
44
- const delta = dateMs - Date.now();
45
- if (delta > 0) return Math.min(MAX_FLUSH_INTERVAL_MS, delta);
46
- }
47
- return undefined;
48
- }
49
-
50
- function classifyResponse(response: Response): SendOutcome {
51
- if (response.ok) return { outcome: "ok" };
52
- const status = response.status;
53
- if (status === 408 || status === 425 || status === 429 || status >= 500) {
54
- return {
55
- outcome: "retry",
56
- retryAfterMs: parseRetryAfter(response.headers.get("retry-after")),
57
- };
58
- }
59
- if (status >= 400 && status < 500) return { outcome: "drop" };
60
- return { outcome: "retry" };
61
- }
62
-
63
- export type WidgetAnalyticsTrackerOptions = {
64
- mode: "embed" | "preview";
65
- agentId: string;
66
- getSession: () => WidgetSessionState | null;
67
- getConversationId: () => string | undefined;
68
- batchSize?: number;
69
- flushIntervalMs?: number;
70
- };
71
-
72
- export class WidgetAnalyticsTracker {
73
- private readonly mode: "embed" | "preview";
74
- private readonly agentId: string;
75
- private readonly getSession: () => WidgetSessionState | null;
76
- private readonly getConversationId: () => string | undefined;
77
- private readonly batchSize: number;
78
- private readonly flushIntervalMs: number;
79
- private readonly uniqueScriptSeen = new Set<string>();
80
- private flushTimer: number | null = null;
81
- private queue: QueuedAnalyticsEvent[] = [];
82
- private disposed = false;
83
- private consecutiveFailures = 0;
84
- private nextAllowedFlushAt = 0;
85
-
86
- constructor(options: WidgetAnalyticsTrackerOptions) {
87
- this.mode = options.mode;
88
- this.agentId = options.agentId;
89
- this.getSession = options.getSession;
90
- this.getConversationId = options.getConversationId;
91
- this.batchSize = Math.max(1, options.batchSize ?? DEFAULT_BATCH_SIZE);
92
- this.flushIntervalMs = Math.max(
93
- 200,
94
- options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
95
- );
96
-
97
- if (ANALYTICS_ENABLED && this.mode === "embed") {
98
- window.addEventListener("pagehide", this.handlePageHide, {
99
- capture: true,
100
- });
101
- document.addEventListener(
102
- "visibilitychange",
103
- this.handleVisibilityChange,
104
- true,
105
- );
106
- }
107
- }
108
-
109
- dispose() {
110
- if (this.disposed) return;
111
- this.disposed = true;
112
- this.clearFlushTimer();
113
- if (!ANALYTICS_ENABLED) return;
114
- window.removeEventListener("pagehide", this.handlePageHide, true);
115
- document.removeEventListener(
116
- "visibilitychange",
117
- this.handleVisibilityChange,
118
- true,
119
- );
120
- void this.flush({ transport: "beacon" });
121
- }
122
-
123
- trackScriptLoaded() {
124
- this.track({ eventType: "SCRIPT_LOADED" });
125
- }
126
-
127
- trackUniqueScriptLoaded() {
128
- if (!ANALYTICS_ENABLED) return;
129
- const sessionToken = this.getSession()?.sessionToken?.trim();
130
- if (!sessionToken) return;
131
- const uniqueKey = `${this.agentId}:${sessionToken}`;
132
-
133
- if (this.uniqueScriptSeen.has(uniqueKey)) return;
134
- if (this.hasSessionUniqueScriptLoaded(uniqueKey)) {
135
- this.uniqueScriptSeen.add(uniqueKey);
136
- return;
137
- }
138
-
139
- this.uniqueScriptSeen.add(uniqueKey);
140
- this.setSessionUniqueScriptLoaded(uniqueKey);
141
- this.track({ eventType: "UNIQUE_SCRIPT_LOADED" });
142
- }
143
-
144
- trackView(triggerRuleType: WidgetTriggerRuleType | null) {
145
- this.track({ eventType: "VIEW", triggerRuleType });
146
- if (triggerRuleType === "element_displayed") {
147
- this.track({
148
- eventType: "DISPLAY_AFTER_ELEMENT_SHOWED",
149
- triggerRuleType,
150
- });
151
- }
152
- }
153
-
154
- trackClick() {
155
- this.track({ eventType: "CLICK" });
156
- }
157
-
158
- trackLinkClicked(linkUrl: string) {
159
- const raw = linkUrl.trim();
160
- if (!raw) return;
161
- const resolvedUrl = this.normalizeLinkUrl(raw);
162
- this.track({ eventType: "LINK_CLICKED", linkUrl: resolvedUrl });
163
- }
164
-
165
- track(event: Omit<WidgetAnalyticsBatchEvent, "occurredAt"> & {
166
- occurredAt?: string;
167
- }) {
168
- if (!ANALYTICS_ENABLED) return;
169
- if (this.disposed || this.mode !== "embed") return;
170
-
171
- const sessionToken = this.getSession()?.sessionToken?.trim();
172
- if (!sessionToken) return;
173
-
174
- const { occurredAt, conversationId, triggerRuleType, linkUrl, ...rest } = event;
175
- const resolvedConversationId = conversationId ?? this.getConversationId();
176
- const queuedEvent: WidgetAnalyticsBatchEvent = {
177
- ...rest,
178
- occurredAt: occurredAt ?? new Date().toISOString(),
179
- ...(resolvedConversationId ? { conversationId: resolvedConversationId } : {}),
180
- ...(triggerRuleType ? { triggerRuleType } : {}),
181
- ...(linkUrl ? { linkUrl } : {}),
182
- };
183
-
184
- this.enqueue({
185
- event: queuedEvent,
186
- sessionToken,
187
- attempts: 0,
188
- enqueuedAt: Date.now(),
189
- });
190
-
191
- if (this.queue.length >= this.batchSize) {
192
- void this.flush();
193
- return;
194
- }
195
-
196
- this.scheduleFlush();
197
- }
198
-
199
- private enqueue(entry: QueuedAnalyticsEvent) {
200
- this.queue.push(entry);
201
- if (this.queue.length > MAX_QUEUE_SIZE) {
202
- this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
203
- }
204
- }
205
-
206
- private pruneStale(now: number) {
207
- if (this.queue.length === 0) return;
208
- this.queue = this.queue.filter(
209
- (entry) => now - entry.enqueuedAt < MAX_EVENT_AGE_MS,
210
- );
211
- }
212
-
213
- async flush(options?: {
214
- transport?: "default" | "beacon";
215
- }): Promise<void> {
216
- if (!ANALYTICS_ENABLED) return;
217
- if (this.disposed) return;
218
-
219
- const now = Date.now();
220
- this.pruneStale(now);
221
- if (this.queue.length === 0) {
222
- this.clearFlushTimer();
223
- return;
224
- }
225
-
226
- this.clearFlushTimer();
227
- const transport = options?.transport ?? "default";
228
- const snapshot = this.queue;
229
- this.queue = [];
230
-
231
- const grouped = new Map<string, QueuedAnalyticsEvent[]>();
232
- for (const entry of snapshot) {
233
- const group = grouped.get(entry.sessionToken);
234
- if (group) {
235
- group.push(entry);
236
- } else {
237
- grouped.set(entry.sessionToken, [entry]);
238
- }
239
- }
240
-
241
- const failed: QueuedAnalyticsEvent[] = [];
242
- let maxRetryAfterMs = 0;
243
- let anyRetry = false;
244
- let anySuccess = false;
245
-
246
- for (const [sessionToken, entries] of grouped.entries()) {
247
- const result = await this.sendBatch({
248
- events: entries.map((entry) => entry.event),
249
- sessionToken,
250
- transport,
251
- });
252
-
253
- if (result.outcome === "ok") {
254
- anySuccess = true;
255
- continue;
256
- }
257
- if (result.outcome === "drop") {
258
- continue;
259
- }
260
- anyRetry = true;
261
- if (result.retryAfterMs && result.retryAfterMs > maxRetryAfterMs) {
262
- maxRetryAfterMs = result.retryAfterMs;
263
- }
264
- for (const entry of entries) {
265
- const attempts = entry.attempts + 1;
266
- if (attempts >= MAX_EVENT_ATTEMPTS) continue;
267
- failed.push({ ...entry, attempts });
268
- }
269
- }
270
-
271
- if (anySuccess && !anyRetry) {
272
- this.consecutiveFailures = 0;
273
- this.nextAllowedFlushAt = 0;
274
- } else if (anyRetry) {
275
- this.consecutiveFailures += 1;
276
- }
277
-
278
- if (failed.length > 0) {
279
- for (const entry of failed) {
280
- this.enqueue(entry);
281
- }
282
- const backoff = this.computeBackoffDelay();
283
- const delay = Math.max(backoff, maxRetryAfterMs);
284
- this.nextAllowedFlushAt = Date.now() + delay;
285
- this.scheduleFlush(delay);
286
- }
287
- }
288
-
289
- private computeBackoffDelay(): number {
290
- const exponent = Math.max(0, this.consecutiveFailures - 1);
291
- const base = this.flushIntervalMs * Math.pow(2, exponent);
292
- const jitter = base * 0.2 * Math.random();
293
- return Math.min(MAX_FLUSH_INTERVAL_MS, base + jitter);
294
- }
295
-
296
- private async sendBatch(input: {
297
- events: WidgetAnalyticsBatchEvent[];
298
- sessionToken: string;
299
- transport: "default" | "beacon";
300
- }): Promise<SendOutcome> {
301
- if (input.events.length === 0) return { outcome: "ok" };
302
-
303
- const url = new URL(ANALYTICS_ENDPOINT, `${getWidgetApiBase()}/`).toString();
304
- const sanitizedEvents = input.events.map((event) => {
305
- const { conversationId, triggerRuleType, linkUrl, ...rest } = event;
306
- return {
307
- ...rest,
308
- ...(conversationId ? { conversationId } : {}),
309
- ...(triggerRuleType ? { triggerRuleType } : {}),
310
- ...(linkUrl ? { linkUrl } : {}),
311
- };
312
- });
313
- const payload: WidgetAnalyticsBatchRequest = {
314
- events: sanitizedEvents,
315
- pageUrl: window.location.href,
316
- referrer: document.referrer || undefined,
317
- // sendBeacon cannot set Authorization header, so include token in body as fallback.
318
- sessionToken: input.sessionToken,
319
- };
320
-
321
- if (input.transport === "beacon" && typeof navigator.sendBeacon === "function") {
322
- const body = new Blob([JSON.stringify(payload)], {
323
- type: "application/json",
324
- });
325
- if (navigator.sendBeacon(url, body)) {
326
- return { outcome: "ok" };
327
- }
328
- }
329
-
330
- try {
331
- const response = await fetch(url, {
332
- method: "POST",
333
- headers: {
334
- Authorization: `Bearer ${input.sessionToken}`,
335
- "Content-Type": "application/json",
336
- },
337
- body: JSON.stringify(payload),
338
- credentials: "omit",
339
- keepalive: input.transport === "beacon",
340
- });
341
- return classifyResponse(response);
342
- } catch {
343
- return { outcome: "retry" };
344
- }
345
- }
346
-
347
- private scheduleFlush(delayMs?: number) {
348
- if (this.flushTimer != null) return;
349
- const delay = Math.max(0, delayMs ?? this.flushIntervalMs);
350
- this.flushTimer = window.setTimeout(() => {
351
- this.flushTimer = null;
352
- void this.flush();
353
- }, delay);
354
- }
355
-
356
- private clearFlushTimer() {
357
- if (this.flushTimer == null) return;
358
- window.clearTimeout(this.flushTimer);
359
- this.flushTimer = null;
360
- }
361
-
362
- private hasSessionUniqueScriptLoaded(uniqueKey: string): boolean {
363
- try {
364
- return (
365
- sessionStorage.getItem(`${UNIQUE_SCRIPT_LOADED_KEY_PREFIX}${uniqueKey}`) ===
366
- "1"
367
- );
368
- } catch {
369
- return false;
370
- }
371
- }
372
-
373
- private setSessionUniqueScriptLoaded(uniqueKey: string) {
374
- try {
375
- sessionStorage.setItem(
376
- `${UNIQUE_SCRIPT_LOADED_KEY_PREFIX}${uniqueKey}`,
377
- "1",
378
- );
379
- } catch {
380
- // no-op
381
- }
382
- }
383
-
384
- private readonly handlePageHide = () => {
385
- void this.flush({ transport: "beacon" });
386
- };
387
-
388
- private readonly handleVisibilityChange = () => {
389
- if (document.visibilityState === "hidden") {
390
- void this.flush({ transport: "beacon" });
391
- }
392
- };
393
-
394
- private normalizeLinkUrl(value: string): string {
395
- try {
396
- return new URL(value, window.location.href).toString();
397
- } catch {
398
- return value;
399
- }
400
- }
401
- }
402
-
403
- export function createWidgetAnalyticsTracker(
404
- options: WidgetAnalyticsTrackerOptions,
405
- ): WidgetAnalyticsTracker {
406
- return new WidgetAnalyticsTracker(options);
407
- }
408
-
409
- export function isWidgetTriggerRuleType(
410
- value: string | null | undefined,
411
- ): value is WidgetTriggerRuleType {
412
- return (
413
- value === "time_delay" ||
414
- value === "scroll_to_bottom" ||
415
- value === "element_clicked" ||
416
- value === "element_displayed"
417
- );
418
- }
419
-
420
- export function isWidgetClientAnalyticsEventType(
421
- value: string | null | undefined,
422
- ): value is WidgetClientAnalyticsEventType {
423
- return (
424
- value === "SCRIPT_LOADED" ||
425
- value === "UNIQUE_SCRIPT_LOADED" ||
426
- value === "VIEW" ||
427
- value === "CLICK" ||
428
- value === "DISPLAY_AFTER_ELEMENT_SHOWED" ||
429
- value === "LINK_CLICKED"
430
- );
431
- }
@@ -1,13 +0,0 @@
1
- export function scheduleRenewal(
2
- expiresAt: string,
3
- onRenew: () => Promise<void> | void
4
- ): () => void {
5
- const expiryMs = new Date(expiresAt).getTime();
6
- const delay = Math.max(5_000, expiryMs - Date.now() - 60_000);
7
- const timer = window.setTimeout(() => {
8
- void onRenew();
9
- }, delay);
10
-
11
- return () => window.clearTimeout(timer);
12
- }
13
-