@probat/react 0.4.5 → 0.4.6

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,157 @@
1
+ import type { StructuredEvent } from "../types/events";
2
+
3
+ interface BatchBody {
4
+ events: StructuredEvent[];
5
+ }
6
+
7
+ export class EventQueue {
8
+ private queue: StructuredEvent[] = [];
9
+ private flushTimer: ReturnType<typeof setTimeout> | null = null;
10
+ private readonly maxBatchSize = 20;
11
+ private readonly flushIntervalMs = 5000;
12
+ private readonly endpointBatch: string;
13
+ private readonly endpointSingle: string;
14
+ private readonly apiKey?: string;
15
+
16
+ constructor(host: string, apiKey?: string) {
17
+ const normalized = host.replace(/\/$/, "");
18
+ this.endpointBatch = `${normalized}/experiment/metrics/batch`;
19
+ this.endpointSingle = `${normalized}/experiment/metrics`;
20
+ this.apiKey = apiKey;
21
+ }
22
+
23
+ enqueue(event: StructuredEvent): void {
24
+ this.queue.push(event);
25
+ if (this.queue.length >= this.maxBatchSize) {
26
+ this.flush();
27
+ return;
28
+ }
29
+ if (!this.flushTimer) {
30
+ this.flushTimer = setTimeout(() => {
31
+ this.flush();
32
+ }, this.flushIntervalMs);
33
+ }
34
+ }
35
+
36
+ flush(forceBeacon = false): void {
37
+ if (this.flushTimer) {
38
+ clearTimeout(this.flushTimer);
39
+ this.flushTimer = null;
40
+ }
41
+ if (this.queue.length === 0) return;
42
+
43
+ const batch = this.queue.splice(0, this.maxBatchSize);
44
+ this.send(batch, forceBeacon);
45
+
46
+ if (this.queue.length > 0) {
47
+ this.flushTimer = setTimeout(() => {
48
+ this.flush();
49
+ }, this.flushIntervalMs);
50
+ }
51
+ }
52
+
53
+ private send(batch: StructuredEvent[], forceBeacon = false): void {
54
+ const body: BatchBody = { events: batch };
55
+ const encodedBody = JSON.stringify(body);
56
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
57
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
58
+ const canBeacon =
59
+ typeof navigator !== "undefined" &&
60
+ typeof navigator.sendBeacon === "function" &&
61
+ !this.apiKey &&
62
+ (
63
+ forceBeacon ||
64
+ (typeof document !== "undefined" && document.visibilityState === "hidden")
65
+ );
66
+
67
+ if (canBeacon) {
68
+ navigator.sendBeacon(this.endpointBatch, encodedBody);
69
+ return;
70
+ }
71
+
72
+ try {
73
+ if (batch.length === 1) {
74
+ fetch(this.endpointSingle, {
75
+ method: "POST",
76
+ headers,
77
+ credentials: "include",
78
+ body: JSON.stringify(batch[0]),
79
+ keepalive: true,
80
+ }).catch(() => {});
81
+ return;
82
+ }
83
+ fetch(this.endpointBatch, {
84
+ method: "POST",
85
+ headers,
86
+ credentials: "include",
87
+ body: encodedBody,
88
+ keepalive: true,
89
+ }).catch(() => {});
90
+ } catch {
91
+ // Deliberately drop failed batches (current behavior parity).
92
+ }
93
+ }
94
+ }
95
+
96
+ const queuesByHost = new Map<string, EventQueue>();
97
+ let lifecycleListenersInstalled = false;
98
+
99
+ function installLifecycleListeners(): void {
100
+ if (lifecycleListenersInstalled || typeof window === "undefined") return;
101
+ lifecycleListenersInstalled = true;
102
+
103
+ const flushWithBeacon = () => {
104
+ for (const queue of queuesByHost.values()) {
105
+ queue.flush(true);
106
+ }
107
+ };
108
+
109
+ const flushNormally = () => {
110
+ for (const queue of queuesByHost.values()) {
111
+ queue.flush(false);
112
+ }
113
+ };
114
+
115
+ window.addEventListener("pagehide", flushWithBeacon);
116
+ window.addEventListener("beforeunload", flushWithBeacon);
117
+ document.addEventListener("visibilitychange", () => {
118
+ if (document.visibilityState === "hidden") {
119
+ flushWithBeacon();
120
+ } else {
121
+ flushNormally();
122
+ }
123
+ });
124
+ }
125
+
126
+ function queueKey(host: string, apiKey?: string): string {
127
+ const normalized = host.replace(/\/$/, "");
128
+ return apiKey ? `${normalized}::${apiKey}` : `${normalized}::__no_key__`;
129
+ }
130
+
131
+ export function getEventQueue(host: string, apiKey?: string): EventQueue {
132
+ const normalized = host.replace(/\/$/, "");
133
+ const key = queueKey(normalized, apiKey);
134
+ let queue = queuesByHost.get(key);
135
+ if (!queue) {
136
+ queue = new EventQueue(normalized, apiKey);
137
+ queuesByHost.set(key, queue);
138
+ }
139
+ installLifecycleListeners();
140
+ return queue;
141
+ }
142
+
143
+ export function flushEventQueue(host: string, forceBeacon = false, apiKey?: string): void {
144
+ const queue = queuesByHost.get(queueKey(host, apiKey));
145
+ if (!queue) return;
146
+ queue.flush(forceBeacon);
147
+ }
148
+
149
+ export function flushAllEventQueues(forceBeacon = false): void {
150
+ for (const queue of queuesByHost.values()) {
151
+ queue.flush(forceBeacon);
152
+ }
153
+ }
154
+
155
+ export function resetEventQueuesForTests(): void {
156
+ queuesByHost.clear();
157
+ }