@klime/node 1.0.3 → 1.2.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 +233 -25
- package/dist/index.cjs +545 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +110 -0
- package/dist/index.d.ts +86 -4
- package/dist/index.js +483 -393
- package/dist/index.js.map +1 -0
- package/package.json +18 -3
- package/dist/types.d.ts +0 -54
- package/dist/types.js +0 -2
package/dist/index.js
CHANGED
|
@@ -1,419 +1,509 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as https from "https";
|
|
3
|
+
import * as http from "http";
|
|
4
|
+
import * as url from "url";
|
|
5
|
+
|
|
6
|
+
// src/types.ts
|
|
7
|
+
var SendError = class extends Error {
|
|
8
|
+
constructor(message, events) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "SendError";
|
|
11
|
+
this.events = events;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/index.ts
|
|
16
|
+
var DEFAULT_ENDPOINT = "https://i.klime.com";
|
|
17
|
+
var DEFAULT_FLUSH_INTERVAL = 2e3;
|
|
18
|
+
var DEFAULT_MAX_BATCH_SIZE = 20;
|
|
19
|
+
var DEFAULT_MAX_QUEUE_SIZE = 1e3;
|
|
20
|
+
var DEFAULT_RETRY_MAX_ATTEMPTS = 5;
|
|
21
|
+
var DEFAULT_RETRY_INITIAL_DELAY = 1e3;
|
|
22
|
+
var MAX_BATCH_SIZE = 100;
|
|
23
|
+
var MAX_EVENT_SIZE_BYTES = 200 * 1024;
|
|
24
|
+
var MAX_BATCH_SIZE_BYTES = 10 * 1024 * 1024;
|
|
25
|
+
var SDK_VERSION = "1.1.0";
|
|
26
|
+
var hasNativeFetch = typeof fetch !== "undefined";
|
|
27
|
+
var createDefaultLogger = () => ({
|
|
28
|
+
debug: (message, ...args) => console.debug(`[Klime] ${message}`, ...args),
|
|
29
|
+
info: (message, ...args) => console.info(`[Klime] ${message}`, ...args),
|
|
30
|
+
warn: (message, ...args) => console.warn(`[Klime] ${message}`, ...args),
|
|
31
|
+
error: (message, ...args) => console.error(`[Klime] ${message}`, ...args)
|
|
17
32
|
});
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
var KlimeClient = class {
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.queue = [];
|
|
36
|
+
this.flushTimer = null;
|
|
37
|
+
this.isShutdown = false;
|
|
38
|
+
this.flushPromise = null;
|
|
39
|
+
this.shutdownHandlers = [];
|
|
40
|
+
if (!config.writeKey) {
|
|
41
|
+
throw new Error("writeKey is required");
|
|
42
|
+
}
|
|
43
|
+
this.config = {
|
|
44
|
+
writeKey: config.writeKey,
|
|
45
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT,
|
|
46
|
+
flushInterval: config.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
|
|
47
|
+
maxBatchSize: Math.min(
|
|
48
|
+
config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
|
|
49
|
+
MAX_BATCH_SIZE
|
|
50
|
+
),
|
|
51
|
+
maxQueueSize: config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
|
|
52
|
+
retryMaxAttempts: config.retryMaxAttempts ?? DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
53
|
+
retryInitialDelay: config.retryInitialDelay ?? DEFAULT_RETRY_INITIAL_DELAY,
|
|
54
|
+
flushOnShutdown: config.flushOnShutdown ?? true,
|
|
55
|
+
logger: config.logger ?? createDefaultLogger(),
|
|
56
|
+
onError: config.onError,
|
|
57
|
+
onSuccess: config.onSuccess
|
|
26
58
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
59
|
+
if (this.config.flushOnShutdown) {
|
|
60
|
+
const shutdownHandler = async () => {
|
|
61
|
+
await this.shutdown();
|
|
62
|
+
};
|
|
63
|
+
process.on("SIGTERM", shutdownHandler);
|
|
64
|
+
process.on("SIGINT", shutdownHandler);
|
|
65
|
+
this.shutdownHandlers.push(() => {
|
|
66
|
+
process.removeListener("SIGTERM", shutdownHandler);
|
|
67
|
+
process.removeListener("SIGINT", shutdownHandler);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
this.scheduleFlush();
|
|
71
|
+
}
|
|
72
|
+
track(event, properties, options) {
|
|
73
|
+
if (this.isShutdown) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const eventObj = {
|
|
77
|
+
type: "track",
|
|
78
|
+
messageId: this.generateUUID(),
|
|
79
|
+
event,
|
|
80
|
+
timestamp: this.generateTimestamp(),
|
|
81
|
+
properties: properties || {},
|
|
82
|
+
context: this.getContext()
|
|
33
83
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
exports.KlimeClient = void 0;
|
|
37
|
-
const https = __importStar(require("https"));
|
|
38
|
-
const http = __importStar(require("http"));
|
|
39
|
-
const url = __importStar(require("url"));
|
|
40
|
-
const DEFAULT_ENDPOINT = "https://i.klime.com";
|
|
41
|
-
const DEFAULT_FLUSH_INTERVAL = 2000;
|
|
42
|
-
const DEFAULT_MAX_BATCH_SIZE = 20;
|
|
43
|
-
const DEFAULT_MAX_QUEUE_SIZE = 1000;
|
|
44
|
-
const DEFAULT_RETRY_MAX_ATTEMPTS = 5;
|
|
45
|
-
const DEFAULT_RETRY_INITIAL_DELAY = 1000;
|
|
46
|
-
const MAX_BATCH_SIZE = 100;
|
|
47
|
-
const MAX_EVENT_SIZE_BYTES = 200 * 1024; // 200KB
|
|
48
|
-
const MAX_BATCH_SIZE_BYTES = 10 * 1024 * 1024; // 10MB
|
|
49
|
-
const SDK_VERSION = "1.0.1";
|
|
50
|
-
// Check if fetch is available (Node 18+)
|
|
51
|
-
const hasNativeFetch = typeof fetch !== "undefined";
|
|
52
|
-
class KlimeClient {
|
|
53
|
-
constructor(config) {
|
|
54
|
-
this.queue = [];
|
|
55
|
-
this.flushTimer = null;
|
|
56
|
-
this.isShutdown = false;
|
|
57
|
-
this.flushPromise = null;
|
|
58
|
-
this.shutdownHandlers = [];
|
|
59
|
-
if (!config.writeKey) {
|
|
60
|
-
throw new Error("writeKey is required");
|
|
61
|
-
}
|
|
62
|
-
this.config = {
|
|
63
|
-
writeKey: config.writeKey,
|
|
64
|
-
endpoint: config.endpoint || DEFAULT_ENDPOINT,
|
|
65
|
-
flushInterval: config.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
|
|
66
|
-
maxBatchSize: Math.min(config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE, MAX_BATCH_SIZE),
|
|
67
|
-
maxQueueSize: config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
|
|
68
|
-
retryMaxAttempts: config.retryMaxAttempts ?? DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
69
|
-
retryInitialDelay: config.retryInitialDelay ?? DEFAULT_RETRY_INITIAL_DELAY,
|
|
70
|
-
flushOnShutdown: config.flushOnShutdown ?? true,
|
|
71
|
-
};
|
|
72
|
-
if (this.config.flushOnShutdown) {
|
|
73
|
-
const shutdownHandler = async () => {
|
|
74
|
-
await this.shutdown();
|
|
75
|
-
};
|
|
76
|
-
process.on("SIGTERM", shutdownHandler);
|
|
77
|
-
process.on("SIGINT", shutdownHandler);
|
|
78
|
-
this.shutdownHandlers.push(() => {
|
|
79
|
-
process.removeListener("SIGTERM", shutdownHandler);
|
|
80
|
-
process.removeListener("SIGINT", shutdownHandler);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
this.scheduleFlush();
|
|
84
|
+
if (options?.userId) {
|
|
85
|
+
eventObj.userId = options.userId;
|
|
84
86
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const eventObj = {
|
|
90
|
-
type: "track",
|
|
91
|
-
messageId: this.generateUUID(),
|
|
92
|
-
event,
|
|
93
|
-
timestamp: this.generateTimestamp(),
|
|
94
|
-
properties: properties || {},
|
|
95
|
-
context: this.getContext(options?.ip),
|
|
96
|
-
};
|
|
97
|
-
if (options?.userId) {
|
|
98
|
-
eventObj.userId = options.userId;
|
|
99
|
-
}
|
|
100
|
-
if (options?.groupId) {
|
|
101
|
-
eventObj.groupId = options.groupId;
|
|
102
|
-
}
|
|
103
|
-
this.enqueue(eventObj);
|
|
87
|
+
if (options?.groupId) {
|
|
88
|
+
eventObj.groupId = options.groupId;
|
|
104
89
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
type: "identify",
|
|
111
|
-
messageId: this.generateUUID(),
|
|
112
|
-
userId,
|
|
113
|
-
timestamp: this.generateTimestamp(),
|
|
114
|
-
traits: traits || {},
|
|
115
|
-
context: this.getContext(options?.ip),
|
|
116
|
-
};
|
|
117
|
-
this.enqueue(eventObj);
|
|
118
|
-
}
|
|
119
|
-
group(groupId, traits, options) {
|
|
120
|
-
if (this.isShutdown) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const eventObj = {
|
|
124
|
-
type: "group",
|
|
125
|
-
messageId: this.generateUUID(),
|
|
126
|
-
groupId,
|
|
127
|
-
timestamp: this.generateTimestamp(),
|
|
128
|
-
traits: traits || {},
|
|
129
|
-
context: this.getContext(options?.ip),
|
|
130
|
-
};
|
|
131
|
-
if (options?.userId) {
|
|
132
|
-
eventObj.userId = options.userId;
|
|
133
|
-
}
|
|
134
|
-
this.enqueue(eventObj);
|
|
90
|
+
this.enqueue(eventObj);
|
|
91
|
+
}
|
|
92
|
+
identify(userId, traits, options) {
|
|
93
|
+
if (this.isShutdown) {
|
|
94
|
+
return;
|
|
135
95
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
96
|
+
const eventObj = {
|
|
97
|
+
type: "identify",
|
|
98
|
+
messageId: this.generateUUID(),
|
|
99
|
+
userId,
|
|
100
|
+
timestamp: this.generateTimestamp(),
|
|
101
|
+
traits: traits || {},
|
|
102
|
+
context: this.getContext()
|
|
103
|
+
};
|
|
104
|
+
this.enqueue(eventObj);
|
|
105
|
+
}
|
|
106
|
+
group(groupId, traits, options) {
|
|
107
|
+
if (this.isShutdown) {
|
|
108
|
+
return;
|
|
147
109
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
// Drop oldest if queue is full
|
|
170
|
-
if (this.queue.length >= this.config.maxQueueSize) {
|
|
171
|
-
this.queue.shift();
|
|
172
|
-
}
|
|
173
|
-
this.queue.push(event);
|
|
174
|
-
// Check if we should flush immediately
|
|
175
|
-
if (this.queue.length >= this.config.maxBatchSize) {
|
|
176
|
-
this.flush();
|
|
177
|
-
}
|
|
110
|
+
const eventObj = {
|
|
111
|
+
type: "group",
|
|
112
|
+
messageId: this.generateUUID(),
|
|
113
|
+
groupId,
|
|
114
|
+
timestamp: this.generateTimestamp(),
|
|
115
|
+
traits: traits || {},
|
|
116
|
+
context: this.getContext()
|
|
117
|
+
};
|
|
118
|
+
if (options?.userId) {
|
|
119
|
+
eventObj.userId = options.userId;
|
|
120
|
+
}
|
|
121
|
+
this.enqueue(eventObj);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Track an event synchronously. Returns BatchResponse or throws SendError.
|
|
125
|
+
*/
|
|
126
|
+
async trackSync(event, properties, options) {
|
|
127
|
+
if (this.isShutdown) {
|
|
128
|
+
throw new SendError("Client is shutdown", []);
|
|
178
129
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
130
|
+
const eventObj = {
|
|
131
|
+
type: "track",
|
|
132
|
+
messageId: this.generateUUID(),
|
|
133
|
+
event,
|
|
134
|
+
timestamp: this.generateTimestamp(),
|
|
135
|
+
properties: properties || {},
|
|
136
|
+
context: this.getContext()
|
|
137
|
+
};
|
|
138
|
+
if (options?.userId) {
|
|
139
|
+
eventObj.userId = options.userId;
|
|
140
|
+
}
|
|
141
|
+
if (options?.groupId) {
|
|
142
|
+
eventObj.groupId = options.groupId;
|
|
143
|
+
}
|
|
144
|
+
return this.sendSync([eventObj]);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Identify a user synchronously. Returns BatchResponse or throws SendError.
|
|
148
|
+
*/
|
|
149
|
+
async identifySync(userId, traits, options) {
|
|
150
|
+
if (this.isShutdown) {
|
|
151
|
+
throw new SendError("Client is shutdown", []);
|
|
152
|
+
}
|
|
153
|
+
const eventObj = {
|
|
154
|
+
type: "identify",
|
|
155
|
+
messageId: this.generateUUID(),
|
|
156
|
+
userId,
|
|
157
|
+
timestamp: this.generateTimestamp(),
|
|
158
|
+
traits: traits || {},
|
|
159
|
+
context: this.getContext()
|
|
160
|
+
};
|
|
161
|
+
return this.sendSync([eventObj]);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Associate a user with a group synchronously. Returns BatchResponse or throws SendError.
|
|
165
|
+
*/
|
|
166
|
+
async groupSync(groupId, traits, options) {
|
|
167
|
+
if (this.isShutdown) {
|
|
168
|
+
throw new SendError("Client is shutdown", []);
|
|
169
|
+
}
|
|
170
|
+
const eventObj = {
|
|
171
|
+
type: "group",
|
|
172
|
+
messageId: this.generateUUID(),
|
|
173
|
+
groupId,
|
|
174
|
+
timestamp: this.generateTimestamp(),
|
|
175
|
+
traits: traits || {},
|
|
176
|
+
context: this.getContext()
|
|
177
|
+
};
|
|
178
|
+
if (options?.userId) {
|
|
179
|
+
eventObj.userId = options.userId;
|
|
180
|
+
}
|
|
181
|
+
return this.sendSync([eventObj]);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Return the number of events currently in the queue.
|
|
185
|
+
*/
|
|
186
|
+
getQueueSize() {
|
|
187
|
+
return this.queue.length;
|
|
188
|
+
}
|
|
189
|
+
async flush() {
|
|
190
|
+
if (this.flushPromise) {
|
|
191
|
+
return this.flushPromise;
|
|
192
|
+
}
|
|
193
|
+
this.flushPromise = this.doFlush();
|
|
194
|
+
try {
|
|
195
|
+
await this.flushPromise;
|
|
196
|
+
} finally {
|
|
197
|
+
this.flushPromise = null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async shutdown() {
|
|
201
|
+
if (this.isShutdown) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.isShutdown = true;
|
|
205
|
+
this.shutdownHandlers.forEach((handler) => handler());
|
|
206
|
+
this.shutdownHandlers = [];
|
|
207
|
+
if (this.flushTimer) {
|
|
208
|
+
clearTimeout(this.flushTimer);
|
|
209
|
+
this.flushTimer = null;
|
|
210
|
+
}
|
|
211
|
+
await this.flush();
|
|
212
|
+
}
|
|
213
|
+
enqueue(event) {
|
|
214
|
+
const eventSize = this.estimateEventSize(event);
|
|
215
|
+
if (eventSize > MAX_EVENT_SIZE_BYTES) {
|
|
216
|
+
this.config.logger.warn(
|
|
217
|
+
`Event size (${eventSize} bytes) exceeds ${MAX_EVENT_SIZE_BYTES} bytes limit`
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
222
|
+
const dropped = this.queue.shift();
|
|
223
|
+
this.config.logger.warn(`Queue full, dropping oldest event: ${dropped?.type}`);
|
|
224
|
+
}
|
|
225
|
+
this.queue.push(event);
|
|
226
|
+
this.config.logger.debug(`Enqueued ${event.type} event, queue size: ${this.queue.length}`);
|
|
227
|
+
if (this.queue.length >= this.config.maxBatchSize) {
|
|
228
|
+
this.flush();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async doFlush() {
|
|
232
|
+
if (this.queue.length === 0) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (this.flushTimer) {
|
|
236
|
+
clearTimeout(this.flushTimer);
|
|
237
|
+
this.flushTimer = null;
|
|
238
|
+
}
|
|
239
|
+
while (this.queue.length > 0) {
|
|
240
|
+
const batch = this.extractBatch();
|
|
241
|
+
if (batch.length === 0) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
await this.sendBatch(batch);
|
|
245
|
+
}
|
|
246
|
+
this.scheduleFlush();
|
|
247
|
+
}
|
|
248
|
+
extractBatch() {
|
|
249
|
+
const batch = [];
|
|
250
|
+
let batchSize = 0;
|
|
251
|
+
while (this.queue.length > 0 && batch.length < MAX_BATCH_SIZE) {
|
|
252
|
+
const event = this.queue[0];
|
|
253
|
+
const eventSize = this.estimateEventSize(event);
|
|
254
|
+
if (batchSize + eventSize > MAX_BATCH_SIZE_BYTES) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
batch.push(this.queue.shift());
|
|
258
|
+
batchSize += eventSize;
|
|
259
|
+
}
|
|
260
|
+
return batch;
|
|
261
|
+
}
|
|
262
|
+
async sendBatch(batch) {
|
|
263
|
+
if (batch.length === 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.config.logger.debug(`Sending batch of ${batch.length} events`);
|
|
267
|
+
const result = await this.doSend(batch);
|
|
268
|
+
if (result === null) {
|
|
269
|
+
const error = new Error(`Failed to send batch of ${batch.length} events after retries`);
|
|
270
|
+
this.invokeOnError(error, batch);
|
|
271
|
+
} else if (result.failed > 0 && result.errors) {
|
|
272
|
+
this.config.logger.warn(
|
|
273
|
+
`Batch partially failed. Accepted: ${result.accepted}, Failed: ${result.failed}`,
|
|
274
|
+
result.errors
|
|
275
|
+
);
|
|
276
|
+
this.invokeOnSuccess(result);
|
|
277
|
+
} else {
|
|
278
|
+
this.config.logger.debug(`Batch sent successfully. Accepted: ${result.accepted}`);
|
|
279
|
+
this.invokeOnSuccess(result);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async sendSync(events) {
|
|
283
|
+
this.config.logger.debug(`Sending ${events.length} events synchronously`);
|
|
284
|
+
const result = await this.doSend(events);
|
|
285
|
+
if (result === null) {
|
|
286
|
+
throw new SendError(`Failed to send ${events.length} events after retries`, events);
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
async doSend(batch) {
|
|
291
|
+
const request = { batch };
|
|
292
|
+
const requestBody = JSON.stringify(request);
|
|
293
|
+
if (hasNativeFetch) {
|
|
294
|
+
return this.doSendWithFetch(batch, requestBody);
|
|
295
|
+
} else {
|
|
296
|
+
return this.doSendWithHttps(batch, requestBody);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async doSendWithFetch(batch, requestBody) {
|
|
300
|
+
const requestUrl = `${this.config.endpoint}/v1/batch`;
|
|
301
|
+
let attempt = 0;
|
|
302
|
+
let delay = this.config.retryInitialDelay;
|
|
303
|
+
while (attempt < this.config.retryMaxAttempts) {
|
|
304
|
+
try {
|
|
305
|
+
const response = await fetch(requestUrl, {
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers: {
|
|
308
|
+
"Content-Type": "application/json",
|
|
309
|
+
Authorization: `Bearer ${this.config.writeKey}`
|
|
310
|
+
},
|
|
311
|
+
body: requestBody
|
|
312
|
+
});
|
|
313
|
+
const data = await response.json();
|
|
314
|
+
if (response.ok) {
|
|
315
|
+
return data;
|
|
316
|
+
}
|
|
317
|
+
if (response.status === 400 || response.status === 401) {
|
|
318
|
+
this.config.logger.error(`Permanent error (${response.status}):`, data);
|
|
319
|
+
return null;
|
|
182
320
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
321
|
+
if (response.status === 429 || response.status === 503) {
|
|
322
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
323
|
+
if (retryAfter) {
|
|
324
|
+
delay = parseInt(retryAfter, 10) * 1e3;
|
|
325
|
+
}
|
|
326
|
+
attempt++;
|
|
327
|
+
if (attempt < this.config.retryMaxAttempts) {
|
|
328
|
+
this.config.logger.debug(`Retrying after ${delay}ms (attempt ${attempt})`);
|
|
329
|
+
await this.sleep(delay);
|
|
330
|
+
delay = Math.min(delay * 2, 16e3);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
187
333
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
await this.sendBatch(batch);
|
|
334
|
+
attempt++;
|
|
335
|
+
if (attempt < this.config.retryMaxAttempts) {
|
|
336
|
+
this.config.logger.debug(`Retrying after ${delay}ms (attempt ${attempt})`);
|
|
337
|
+
await this.sleep(delay);
|
|
338
|
+
delay = Math.min(delay * 2, 16e3);
|
|
195
339
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const eventSize = this.estimateEventSize(event);
|
|
205
|
-
// Check if adding this event would exceed batch size limit
|
|
206
|
-
if (batchSize + eventSize > MAX_BATCH_SIZE_BYTES) {
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
batch.push(this.queue.shift());
|
|
210
|
-
batchSize += eventSize;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
attempt++;
|
|
342
|
+
if (attempt < this.config.retryMaxAttempts) {
|
|
343
|
+
this.config.logger.debug(`Retrying after ${delay}ms (attempt ${attempt})`);
|
|
344
|
+
await this.sleep(delay);
|
|
345
|
+
delay = Math.min(delay * 2, 16e3);
|
|
346
|
+
} else {
|
|
347
|
+
this.config.logger.error("Failed to send batch after retries:", error);
|
|
211
348
|
}
|
|
212
|
-
|
|
349
|
+
}
|
|
213
350
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
async doSendWithHttps(batch, requestBody) {
|
|
354
|
+
const parsedUrl = url.parse(this.config.endpoint);
|
|
355
|
+
const isHttps = parsedUrl.protocol === "https:";
|
|
356
|
+
const client = isHttps ? https : http;
|
|
357
|
+
const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : isHttps ? 443 : 80;
|
|
358
|
+
const hostname = parsedUrl.hostname || "";
|
|
359
|
+
let attempt = 0;
|
|
360
|
+
let delay = this.config.retryInitialDelay;
|
|
361
|
+
while (attempt < this.config.retryMaxAttempts) {
|
|
362
|
+
try {
|
|
363
|
+
const result = await this.makeRequest(
|
|
364
|
+
client,
|
|
365
|
+
hostname,
|
|
366
|
+
port,
|
|
367
|
+
"/v1/batch",
|
|
368
|
+
requestBody
|
|
369
|
+
);
|
|
370
|
+
if (result.statusCode === 200) {
|
|
371
|
+
const data = JSON.parse(result.body);
|
|
372
|
+
return data;
|
|
217
373
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
374
|
+
if (result.statusCode === 400 || result.statusCode === 401) {
|
|
375
|
+
const data = JSON.parse(result.body);
|
|
376
|
+
this.config.logger.error(`Permanent error (${result.statusCode}):`, data);
|
|
377
|
+
return null;
|
|
222
378
|
}
|
|
223
|
-
|
|
224
|
-
|
|
379
|
+
if (result.statusCode === 429 || result.statusCode === 503) {
|
|
380
|
+
const retryAfter = result.headers["retry-after"];
|
|
381
|
+
if (retryAfter) {
|
|
382
|
+
delay = parseInt(
|
|
383
|
+
Array.isArray(retryAfter) ? retryAfter[0] : retryAfter,
|
|
384
|
+
10
|
|
385
|
+
) * 1e3;
|
|
386
|
+
}
|
|
387
|
+
attempt++;
|
|
388
|
+
if (attempt < this.config.retryMaxAttempts) {
|
|
389
|
+
this.config.logger.debug(`Retrying after ${delay}ms (attempt ${attempt})`);
|
|
390
|
+
await this.sleep(delay);
|
|
391
|
+
delay = Math.min(delay * 2, 16e3);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
225
394
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
while (attempt < this.config.retryMaxAttempts) {
|
|
232
|
-
try {
|
|
233
|
-
const response = await fetch(requestUrl, {
|
|
234
|
-
method: "POST",
|
|
235
|
-
headers: {
|
|
236
|
-
"Content-Type": "application/json",
|
|
237
|
-
Authorization: `Bearer ${this.config.writeKey}`,
|
|
238
|
-
},
|
|
239
|
-
body: requestBody,
|
|
240
|
-
});
|
|
241
|
-
const data = (await response.json());
|
|
242
|
-
if (response.ok) {
|
|
243
|
-
if (data.failed > 0 && data.errors) {
|
|
244
|
-
console.warn(`Klime: Batch partially failed. Accepted: ${data.accepted}, Failed: ${data.failed}`, data.errors);
|
|
245
|
-
}
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (response.status === 400 || response.status === 401) {
|
|
249
|
-
console.error(`Klime: Permanent error (${response.status}):`, data);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
if (response.status === 429 || response.status === 503) {
|
|
253
|
-
const retryAfter = response.headers.get("Retry-After");
|
|
254
|
-
if (retryAfter) {
|
|
255
|
-
delay = parseInt(retryAfter, 10) * 1000;
|
|
256
|
-
}
|
|
257
|
-
attempt++;
|
|
258
|
-
if (attempt < this.config.retryMaxAttempts) {
|
|
259
|
-
await this.sleep(delay);
|
|
260
|
-
delay = Math.min(delay * 2, 16000);
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
attempt++;
|
|
265
|
-
if (attempt < this.config.retryMaxAttempts) {
|
|
266
|
-
await this.sleep(delay);
|
|
267
|
-
delay = Math.min(delay * 2, 16000);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
attempt++;
|
|
272
|
-
if (attempt < this.config.retryMaxAttempts) {
|
|
273
|
-
await this.sleep(delay);
|
|
274
|
-
delay = Math.min(delay * 2, 16000);
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
console.error("Klime: Failed to send batch after retries:", error);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
395
|
+
attempt++;
|
|
396
|
+
if (attempt < this.config.retryMaxAttempts) {
|
|
397
|
+
this.config.logger.debug(`Retrying after ${delay}ms (attempt ${attempt})`);
|
|
398
|
+
await this.sleep(delay);
|
|
399
|
+
delay = Math.min(delay * 2, 16e3);
|
|
280
400
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
? 443
|
|
290
|
-
: 80;
|
|
291
|
-
const hostname = parsedUrl.hostname || "";
|
|
292
|
-
let attempt = 0;
|
|
293
|
-
let delay = this.config.retryInitialDelay;
|
|
294
|
-
while (attempt < this.config.retryMaxAttempts) {
|
|
295
|
-
try {
|
|
296
|
-
const result = await this.makeRequest(client, hostname, port, "/v1/batch", requestBody);
|
|
297
|
-
if (result.statusCode === 200) {
|
|
298
|
-
const data = JSON.parse(result.body);
|
|
299
|
-
if (data.failed > 0 && data.errors) {
|
|
300
|
-
console.warn(`Klime: Batch partially failed. Accepted: ${data.accepted}, Failed: ${data.failed}`, data.errors);
|
|
301
|
-
}
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (result.statusCode === 400 || result.statusCode === 401) {
|
|
305
|
-
const data = JSON.parse(result.body);
|
|
306
|
-
console.error(`Klime: Permanent error (${result.statusCode}):`, data);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (result.statusCode === 429 || result.statusCode === 503) {
|
|
310
|
-
const retryAfter = result.headers["retry-after"];
|
|
311
|
-
if (retryAfter) {
|
|
312
|
-
delay =
|
|
313
|
-
parseInt(Array.isArray(retryAfter) ? retryAfter[0] : retryAfter, 10) * 1000;
|
|
314
|
-
}
|
|
315
|
-
attempt++;
|
|
316
|
-
if (attempt < this.config.retryMaxAttempts) {
|
|
317
|
-
await this.sleep(delay);
|
|
318
|
-
delay = Math.min(delay * 2, 16000);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
attempt++;
|
|
323
|
-
if (attempt < this.config.retryMaxAttempts) {
|
|
324
|
-
await this.sleep(delay);
|
|
325
|
-
delay = Math.min(delay * 2, 16000);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
catch (error) {
|
|
329
|
-
attempt++;
|
|
330
|
-
if (attempt < this.config.retryMaxAttempts) {
|
|
331
|
-
await this.sleep(delay);
|
|
332
|
-
delay = Math.min(delay * 2, 16000);
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
console.error("Klime: Failed to send batch after retries:", error);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
attempt++;
|
|
403
|
+
if (attempt < this.config.retryMaxAttempts) {
|
|
404
|
+
this.config.logger.debug(`Retrying after ${delay}ms (attempt ${attempt})`);
|
|
405
|
+
await this.sleep(delay);
|
|
406
|
+
delay = Math.min(delay * 2, 16e3);
|
|
407
|
+
} else {
|
|
408
|
+
this.config.logger.error("Failed to send batch after retries:", error);
|
|
338
409
|
}
|
|
410
|
+
}
|
|
339
411
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
Authorization: `Bearer ${this.config.writeKey}`,
|
|
350
|
-
"Content-Length": Buffer.byteLength(body),
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
const req = client.request(options, (res) => {
|
|
354
|
-
let data = "";
|
|
355
|
-
res.on("data", (chunk) => {
|
|
356
|
-
data += chunk;
|
|
357
|
-
});
|
|
358
|
-
res.on("end", () => {
|
|
359
|
-
resolve({
|
|
360
|
-
statusCode: res.statusCode || 500,
|
|
361
|
-
body: data,
|
|
362
|
-
headers: res.headers,
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
req.on("error", (error) => {
|
|
367
|
-
reject(error);
|
|
368
|
-
});
|
|
369
|
-
req.write(body);
|
|
370
|
-
req.end();
|
|
371
|
-
});
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
invokeOnError(error, batch) {
|
|
415
|
+
if (this.config.onError) {
|
|
416
|
+
try {
|
|
417
|
+
this.config.onError(error, batch);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
this.config.logger.error("Error in onError callback:", e);
|
|
420
|
+
}
|
|
372
421
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
this.
|
|
378
|
-
|
|
379
|
-
|
|
422
|
+
}
|
|
423
|
+
invokeOnSuccess(response) {
|
|
424
|
+
if (this.config.onSuccess) {
|
|
425
|
+
try {
|
|
426
|
+
this.config.onSuccess(response);
|
|
427
|
+
} catch (e) {
|
|
428
|
+
this.config.logger.error("Error in onSuccess callback:", e);
|
|
429
|
+
}
|
|
380
430
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
431
|
+
}
|
|
432
|
+
makeRequest(client, hostname, port, path, body) {
|
|
433
|
+
return new Promise((resolve, reject) => {
|
|
434
|
+
const options = {
|
|
435
|
+
hostname,
|
|
436
|
+
port,
|
|
437
|
+
path,
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: {
|
|
440
|
+
"Content-Type": "application/json",
|
|
441
|
+
Authorization: `Bearer ${this.config.writeKey}`,
|
|
442
|
+
"Content-Length": Buffer.byteLength(body)
|
|
384
443
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
444
|
+
};
|
|
445
|
+
const req = client.request(options, (res) => {
|
|
446
|
+
let data = "";
|
|
447
|
+
res.on("data", (chunk) => {
|
|
448
|
+
data += chunk;
|
|
449
|
+
});
|
|
450
|
+
res.on("end", () => {
|
|
451
|
+
resolve({
|
|
452
|
+
statusCode: res.statusCode || 500,
|
|
453
|
+
body: data,
|
|
454
|
+
headers: res.headers
|
|
455
|
+
});
|
|
390
456
|
});
|
|
457
|
+
});
|
|
458
|
+
req.on("error", (error) => {
|
|
459
|
+
reject(error);
|
|
460
|
+
});
|
|
461
|
+
req.write(body);
|
|
462
|
+
req.end();
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
scheduleFlush() {
|
|
466
|
+
if (this.isShutdown || this.flushTimer) {
|
|
467
|
+
return;
|
|
391
468
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
version: SDK_VERSION,
|
|
400
|
-
},
|
|
401
|
-
};
|
|
402
|
-
if (ip) {
|
|
403
|
-
context.ip = ip;
|
|
404
|
-
}
|
|
405
|
-
return context;
|
|
469
|
+
this.flushTimer = setTimeout(() => {
|
|
470
|
+
this.flush();
|
|
471
|
+
}, this.config.flushInterval);
|
|
472
|
+
}
|
|
473
|
+
generateUUID() {
|
|
474
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
475
|
+
return crypto.randomUUID();
|
|
406
476
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
477
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
478
|
+
const r = Math.random() * 16 | 0;
|
|
479
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
480
|
+
return v.toString(16);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
generateTimestamp() {
|
|
484
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
485
|
+
}
|
|
486
|
+
getContext() {
|
|
487
|
+
return {
|
|
488
|
+
library: {
|
|
489
|
+
name: "node-sdk",
|
|
490
|
+
version: SDK_VERSION
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
estimateEventSize(event) {
|
|
495
|
+
try {
|
|
496
|
+
return JSON.stringify(event).length;
|
|
497
|
+
} catch {
|
|
498
|
+
return 500;
|
|
417
499
|
}
|
|
418
|
-
}
|
|
419
|
-
|
|
500
|
+
}
|
|
501
|
+
sleep(ms) {
|
|
502
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
export {
|
|
506
|
+
KlimeClient,
|
|
507
|
+
SendError
|
|
508
|
+
};
|
|
509
|
+
//# sourceMappingURL=index.js.map
|