logtap-sdk 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 (4) hide show
  1. package/README.md +55 -0
  2. package/index.d.ts +108 -0
  3. package/index.js +526 -0
  4. package/package.json +38 -0
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # logtap JS SDK(Browser / Node)
2
+
3
+ 用于向 logtap 网关上报「结构化日志」与「埋点事件」。
4
+
5
+ ## 配置
6
+
7
+ ```js
8
+ import { LogtapClient } from "logtap-sdk";
9
+
10
+ const client = new LogtapClient({
11
+ baseUrl: "http://localhost:8080",
12
+ projectId: 1,
13
+ projectKey: "pk_xxx", // 启用 AUTH_SECRET 时必填
14
+ gzip: true, // 浏览器需要支持 CompressionStream;否则会自动降级为非 gzip
15
+ globalContexts: { app: { version: "1.2.3" } },
16
+ });
17
+ ```
18
+
19
+ ## 日志
20
+
21
+ ```js
22
+ client.info("hello", { k: "v" });
23
+ client.error("boom", { err: String(err), stack: err?.stack });
24
+ ```
25
+
26
+ ## 埋点
27
+
28
+ ```js
29
+ client.identify("u1", { plan: "pro" });
30
+ client.track("signup", { from: "landing" });
31
+ ```
32
+
33
+ 也支持在单次调用里带 `contexts/extra/user/deviceId`(覆盖全局默认值):
34
+
35
+ ```js
36
+ client.info("hello", { k: "v" }, { contexts: { page: { path: location.pathname } } });
37
+ ```
38
+
39
+ 事件上报会调用 `POST /api/:projectId/track/`,服务端会以 `logs.level=event` 落库并用于事件分析/漏斗。
40
+
41
+ ## 自动捕获(可选)
42
+
43
+ ```js
44
+ client.captureBrowserErrors(); // 浏览器:window.error / unhandledrejection
45
+ client.captureNodeErrors(); // Node:unhandledRejection / uncaughtException
46
+ ```
47
+
48
+ ## Flush / 退出前发送
49
+
50
+ SDK 默认每 2 秒自动 flush;也可手动调用:
51
+
52
+ ```js
53
+ await client.flush();
54
+ await client.close();
55
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,108 @@
1
+ export type LogtapLevel = "debug" | "info" | "warn" | "error" | "fatal" | "event";
2
+
3
+ export interface LogtapUser {
4
+ id?: string;
5
+ email?: string;
6
+ username?: string;
7
+ traits?: Record<string, any>;
8
+ }
9
+
10
+ export interface LogtapLog {
11
+ level: LogtapLevel;
12
+ message: string;
13
+ timestamp?: string;
14
+ device_id?: string;
15
+ trace_id?: string;
16
+ span_id?: string;
17
+ fields?: Record<string, any>;
18
+ tags?: Record<string, string>;
19
+ user?: Record<string, any>;
20
+ contexts?: Record<string, any>;
21
+ extra?: Record<string, any>;
22
+ sdk?: Record<string, any>;
23
+ }
24
+
25
+ export interface LogtapTrackEvent {
26
+ name: string;
27
+ timestamp?: string;
28
+ device_id?: string;
29
+ trace_id?: string;
30
+ span_id?: string;
31
+ properties?: Record<string, any>;
32
+ tags?: Record<string, string>;
33
+ user?: Record<string, any>;
34
+ contexts?: Record<string, any>;
35
+ extra?: Record<string, any>;
36
+ sdk?: Record<string, any>;
37
+ }
38
+
39
+ export interface LogtapClientOptions {
40
+ baseUrl: string;
41
+ projectId: number | string;
42
+ projectKey?: string;
43
+ flushIntervalMs?: number;
44
+ maxBatchSize?: number;
45
+ maxQueueSize?: number;
46
+ timeoutMs?: number;
47
+ gzip?: boolean;
48
+ persistDeviceId?: boolean;
49
+ deviceId?: string;
50
+ user?: LogtapUser;
51
+ globalFields?: Record<string, any>;
52
+ globalProperties?: Record<string, any>;
53
+ globalTags?: Record<string, string>;
54
+ globalContexts?: Record<string, any>;
55
+ beforeSend?: (payload: LogtapLog | LogtapTrackEvent) => LogtapLog | LogtapTrackEvent | null;
56
+ }
57
+
58
+ export class LogtapClient {
59
+ constructor(options: LogtapClientOptions);
60
+
61
+ setUser(user?: LogtapUser | null): void;
62
+ identify(userId: string, traits?: Record<string, any>): void;
63
+ clearUser(): void;
64
+ setDeviceId(deviceId: string, options?: { persist?: boolean }): void;
65
+
66
+ log(
67
+ level: LogtapLevel,
68
+ message: string,
69
+ fields?: Record<string, any>,
70
+ options?: {
71
+ traceId?: string;
72
+ spanId?: string;
73
+ timestamp?: string | Date;
74
+ tags?: Record<string, string>;
75
+ deviceId?: string;
76
+ user?: LogtapUser;
77
+ contexts?: Record<string, any>;
78
+ extra?: Record<string, any>;
79
+ },
80
+ ): void;
81
+
82
+ debug(message: string, fields?: Record<string, any>, options?: { traceId?: string; spanId?: string; timestamp?: string | Date; tags?: Record<string, string>; deviceId?: string; user?: LogtapUser; contexts?: Record<string, any>; extra?: Record<string, any> }): void;
83
+ info(message: string, fields?: Record<string, any>, options?: { traceId?: string; spanId?: string; timestamp?: string | Date; tags?: Record<string, string>; deviceId?: string; user?: LogtapUser; contexts?: Record<string, any>; extra?: Record<string, any> }): void;
84
+ warn(message: string, fields?: Record<string, any>, options?: { traceId?: string; spanId?: string; timestamp?: string | Date; tags?: Record<string, string>; deviceId?: string; user?: LogtapUser; contexts?: Record<string, any>; extra?: Record<string, any> }): void;
85
+ error(message: string, fields?: Record<string, any>, options?: { traceId?: string; spanId?: string; timestamp?: string | Date; tags?: Record<string, string>; deviceId?: string; user?: LogtapUser; contexts?: Record<string, any>; extra?: Record<string, any> }): void;
86
+ fatal(message: string, fields?: Record<string, any>, options?: { traceId?: string; spanId?: string; timestamp?: string | Date; tags?: Record<string, string>; deviceId?: string; user?: LogtapUser; contexts?: Record<string, any>; extra?: Record<string, any> }): void;
87
+
88
+ track(
89
+ name: string,
90
+ properties?: Record<string, any>,
91
+ options?: {
92
+ traceId?: string;
93
+ spanId?: string;
94
+ timestamp?: string | Date;
95
+ tags?: Record<string, string>;
96
+ deviceId?: string;
97
+ user?: LogtapUser;
98
+ contexts?: Record<string, any>;
99
+ extra?: Record<string, any>;
100
+ },
101
+ ): void;
102
+
103
+ flush(): Promise<void>;
104
+ close(): Promise<void>;
105
+
106
+ captureBrowserErrors(options?: { includeSource?: boolean }): void;
107
+ captureNodeErrors(): void;
108
+ }
package/index.js ADDED
@@ -0,0 +1,526 @@
1
+ const SDK_NAME = "logtap-sdk";
2
+ const SDK_VERSION = "0.1.0";
3
+
4
+ /**
5
+ * @typedef {"debug"|"info"|"warn"|"error"|"fatal"|"event"} LogtapLevel
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} LogtapUser
10
+ * @property {string=} id
11
+ * @property {string=} email
12
+ * @property {string=} username
13
+ * @property {Record<string, any>=} traits
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} LogtapLog
18
+ * @property {LogtapLevel} level
19
+ * @property {string} message
20
+ * @property {string=} timestamp RFC3339
21
+ * @property {string=} device_id
22
+ * @property {string=} trace_id
23
+ * @property {string=} span_id
24
+ * @property {Record<string, any>=} fields
25
+ * @property {Record<string, string>=} tags
26
+ * @property {Record<string, any>=} user
27
+ * @property {Record<string, any>=} contexts
28
+ * @property {Record<string, any>=} extra
29
+ * @property {Record<string, any>=} sdk
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} LogtapTrackEvent
34
+ * @property {string} name
35
+ * @property {string=} timestamp RFC3339
36
+ * @property {string=} device_id
37
+ * @property {string=} trace_id
38
+ * @property {string=} span_id
39
+ * @property {Record<string, any>=} properties
40
+ * @property {Record<string, string>=} tags
41
+ * @property {Record<string, any>=} user
42
+ * @property {Record<string, any>=} contexts
43
+ * @property {Record<string, any>=} extra
44
+ * @property {Record<string, any>=} sdk
45
+ */
46
+
47
+ /**
48
+ * @typedef {Object} LogtapClientOptions
49
+ * @property {string} baseUrl e.g. "http://localhost:8080"
50
+ * @property {number|string} projectId
51
+ * @property {string=} projectKey sent as X-Project-Key: pk_...
52
+ * @property {number=} flushIntervalMs default 2000
53
+ * @property {number=} maxBatchSize default 50
54
+ * @property {number=} maxQueueSize default 1000 per queue
55
+ * @property {number=} timeoutMs default 5000
56
+ * @property {boolean=} gzip default false (browser requires CompressionStream)
57
+ * @property {boolean=} persistDeviceId default true (browser only)
58
+ * @property {string=} deviceId
59
+ * @property {LogtapUser=} user
60
+ * @property {Record<string, any>=} globalFields merged into every log fields
61
+ * @property {Record<string, any>=} globalProperties merged into every track properties
62
+ * @property {Record<string, string>=} globalTags merged into every payload tags
63
+ * @property {Record<string, any>=} globalContexts merged into every payload contexts
64
+ * @property {(payload: LogtapLog|LogtapTrackEvent) => (LogtapLog|LogtapTrackEvent|null)=} beforeSend
65
+ */
66
+
67
+ function isBrowser() {
68
+ return typeof window !== "undefined" && typeof window.document !== "undefined";
69
+ }
70
+
71
+ function nowISO() {
72
+ return new Date().toISOString();
73
+ }
74
+
75
+ function normalizeBaseUrl(baseUrl) {
76
+ const s = String(baseUrl || "").trim();
77
+ if (!s) throw new Error("baseUrl required");
78
+ return s.endsWith("/") ? s.slice(0, -1) : s;
79
+ }
80
+
81
+ function sleep(ms) {
82
+ return new Promise((resolve) => setTimeout(resolve, ms));
83
+ }
84
+
85
+ function safeJson(value) {
86
+ try {
87
+ return JSON.parse(JSON.stringify(value));
88
+ } catch {
89
+ return undefined;
90
+ }
91
+ }
92
+
93
+ function randomHex(bytes) {
94
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
95
+ const buf = new Uint8Array(bytes);
96
+ globalThis.crypto.getRandomValues(buf);
97
+ return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
98
+ }
99
+ return Array.from({ length: bytes }, () => Math.floor(Math.random() * 256))
100
+ .map((b) => b.toString(16).padStart(2, "0"))
101
+ .join("");
102
+ }
103
+
104
+ function newDeviceId() {
105
+ return `d_${randomHex(16)}`;
106
+ }
107
+
108
+ function mergeObj(a, b) {
109
+ if (!a && !b) return undefined;
110
+ /** @type {Record<string, any>} */
111
+ const out = {};
112
+ if (a && typeof a === "object") Object.assign(out, a);
113
+ if (b && typeof b === "object") Object.assign(out, b);
114
+ return out;
115
+ }
116
+
117
+ function mergeTags(a, b) {
118
+ if (!a && !b) return undefined;
119
+ /** @type {Record<string, string>} */
120
+ const out = {};
121
+ if (a && typeof a === "object") Object.assign(out, a);
122
+ if (b && typeof b === "object") Object.assign(out, b);
123
+ return out;
124
+ }
125
+
126
+ async function gzipIfEnabled(bodyString, enabled) {
127
+ if (!enabled) return { body: bodyString, contentEncoding: undefined };
128
+
129
+ if (isBrowser()) {
130
+ if (typeof CompressionStream !== "function") {
131
+ return { body: bodyString, contentEncoding: undefined };
132
+ }
133
+ const stream = new CompressionStream("gzip");
134
+ const writer = stream.writable.getWriter();
135
+ const enc = new TextEncoder();
136
+ await writer.write(enc.encode(bodyString));
137
+ await writer.close();
138
+ const buf = await new Response(stream.readable).arrayBuffer();
139
+ return { body: new Uint8Array(buf), contentEncoding: "gzip" };
140
+ }
141
+
142
+ const { gzipSync } = await import("node:zlib");
143
+ const { Buffer } = await import("node:buffer");
144
+ const gz = gzipSync(Buffer.from(bodyString, "utf8"));
145
+ return { body: gz, contentEncoding: "gzip" };
146
+ }
147
+
148
+ /**
149
+ * Browser/Node client for logtap logs + tracking.
150
+ */
151
+ export class LogtapClient {
152
+ /**
153
+ * @param {LogtapClientOptions} options
154
+ */
155
+ constructor(options) {
156
+ this._baseUrl = normalizeBaseUrl(options.baseUrl);
157
+ this._projectId = String(options.projectId);
158
+ if (!this._projectId) throw new Error("projectId required");
159
+ this._projectKey = options.projectKey ? String(options.projectKey).trim() : "";
160
+ this._timeoutMs = Number(options.timeoutMs ?? 5000);
161
+ this._flushIntervalMs = Number(options.flushIntervalMs ?? 2000);
162
+ this._maxBatchSize = Number(options.maxBatchSize ?? 50);
163
+ this._maxQueueSize = Number(options.maxQueueSize ?? 1000);
164
+ this._gzip = Boolean(options.gzip ?? false);
165
+ this._persistDeviceId = Boolean(options.persistDeviceId ?? true);
166
+ this._beforeSend = typeof options.beforeSend === "function" ? options.beforeSend : null;
167
+
168
+ this._globalFields = options.globalFields && typeof options.globalFields === "object" ? options.globalFields : undefined;
169
+ this._globalProperties =
170
+ options.globalProperties && typeof options.globalProperties === "object" ? options.globalProperties : undefined;
171
+ this._globalTags = options.globalTags && typeof options.globalTags === "object" ? options.globalTags : undefined;
172
+ this._globalContexts = options.globalContexts && typeof options.globalContexts === "object" ? options.globalContexts : undefined;
173
+
174
+ this._user = options.user ? safeJson(options.user) : undefined;
175
+ this._deviceId = options.deviceId ? String(options.deviceId) : this._loadOrCreateDeviceId();
176
+
177
+ /** @type {LogtapLog[]} */
178
+ this._logQueue = [];
179
+ /** @type {LogtapTrackEvent[]} */
180
+ this._trackQueue = [];
181
+
182
+ this._backoffMs = 0;
183
+ this._flushing = null;
184
+ this._timer = null;
185
+
186
+ if (this._flushIntervalMs > 0) {
187
+ this._timer = setInterval(() => void this.flush(), this._flushIntervalMs);
188
+ // In Node, don't keep the process alive for the timer.
189
+ if (!isBrowser() && typeof this._timer?.unref === "function") {
190
+ this._timer.unref();
191
+ }
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Set/overwrite current user context (merged into every payload).
197
+ * @param {LogtapUser|null|undefined} user
198
+ */
199
+ setUser(user) {
200
+ this._user = user ? safeJson(user) : undefined;
201
+ }
202
+
203
+ /**
204
+ * Convenience identity method.
205
+ * @param {string} userId
206
+ * @param {Record<string, any>=} traits
207
+ */
208
+ identify(userId, traits) {
209
+ const id = String(userId || "").trim();
210
+ if (!id) return;
211
+ /** @type {LogtapUser} */
212
+ const u = { id };
213
+ if (traits && typeof traits === "object") u.traits = safeJson(traits);
214
+ this.setUser(u);
215
+ }
216
+
217
+ clearUser() {
218
+ this._user = undefined;
219
+ }
220
+
221
+ /**
222
+ * Override device_id (used for DAU/MAU distinct).
223
+ * @param {string} deviceId
224
+ * @param {{persist?: boolean}=} options
225
+ */
226
+ setDeviceId(deviceId, options) {
227
+ const id = String(deviceId || "").trim();
228
+ if (!id) return;
229
+ this._deviceId = id;
230
+ const persist = options?.persist ?? this._persistDeviceId;
231
+ if (persist && isBrowser()) {
232
+ try {
233
+ localStorage.setItem("logtap_device_id", id);
234
+ } catch {}
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Enqueue a structured log (sent to POST /api/:projectId/logs/).
240
+ * @param {LogtapLevel} level
241
+ * @param {string} message
242
+ * @param {Record<string, any>=} fields
243
+ * @param {{traceId?: string, spanId?: string, timestamp?: string|Date, tags?: Record<string,string>, deviceId?: string, user?: LogtapUser, contexts?: Record<string, any>, extra?: Record<string, any>}=} options
244
+ */
245
+ log(level, message, fields, options) {
246
+ const msg = String(message || "").trim();
247
+ if (!msg) return;
248
+
249
+ /** @type {LogtapLog} */
250
+ const payload = {
251
+ level: level || "info",
252
+ message: msg,
253
+ device_id: options?.deviceId ? String(options.deviceId) : this._deviceId,
254
+ trace_id: options?.traceId ? String(options.traceId) : undefined,
255
+ span_id: options?.spanId ? String(options.spanId) : undefined,
256
+ timestamp: toTimestamp(options?.timestamp) ?? nowISO(),
257
+ fields: mergeObj(this._globalFields, safeJson(fields)),
258
+ tags: mergeTags(this._globalTags, options?.tags),
259
+ user: options?.user ? safeJson(options.user) : this._user,
260
+ contexts: mergeObj(this._globalContexts, safeJson(options?.contexts)),
261
+ extra: safeJson(options?.extra),
262
+ sdk: { name: SDK_NAME, version: SDK_VERSION, runtime: isBrowser() ? "browser" : "node" },
263
+ };
264
+
265
+ this._enqueueLog(payload);
266
+ }
267
+
268
+ debug(message, fields, options) {
269
+ this.log("debug", message, fields, options);
270
+ }
271
+ info(message, fields, options) {
272
+ this.log("info", message, fields, options);
273
+ }
274
+ warn(message, fields, options) {
275
+ this.log("warn", message, fields, options);
276
+ }
277
+ error(message, fields, options) {
278
+ this.log("error", message, fields, options);
279
+ }
280
+ fatal(message, fields, options) {
281
+ this.log("fatal", message, fields, options);
282
+ }
283
+
284
+ /**
285
+ * Track an event (sent to POST /api/:projectId/track/).
286
+ * @param {string} name
287
+ * @param {Record<string, any>=} properties
288
+ * @param {{traceId?: string, spanId?: string, timestamp?: string|Date, tags?: Record<string,string>, deviceId?: string, user?: LogtapUser, contexts?: Record<string, any>, extra?: Record<string, any>}=} options
289
+ */
290
+ track(name, properties, options) {
291
+ const n = String(name || "").trim();
292
+ if (!n) return;
293
+
294
+ /** @type {LogtapTrackEvent} */
295
+ const payload = {
296
+ name: n,
297
+ device_id: options?.deviceId ? String(options.deviceId) : this._deviceId,
298
+ trace_id: options?.traceId ? String(options.traceId) : undefined,
299
+ span_id: options?.spanId ? String(options.spanId) : undefined,
300
+ timestamp: toTimestamp(options?.timestamp) ?? nowISO(),
301
+ properties: mergeObj(this._globalProperties, safeJson(properties)),
302
+ tags: mergeTags(this._globalTags, options?.tags),
303
+ user: options?.user ? safeJson(options.user) : this._user,
304
+ contexts: mergeObj(this._globalContexts, safeJson(options?.contexts)),
305
+ extra: safeJson(options?.extra),
306
+ sdk: { name: SDK_NAME, version: SDK_VERSION, runtime: isBrowser() ? "browser" : "node" },
307
+ };
308
+
309
+ this._enqueueTrack(payload);
310
+ }
311
+
312
+ /**
313
+ * Flush queued logs + events now.
314
+ * @returns {Promise<void>}
315
+ */
316
+ async flush() {
317
+ if (this._flushing) return this._flushing;
318
+ this._flushing = this._flushInner().finally(() => {
319
+ this._flushing = null;
320
+ });
321
+ return this._flushing;
322
+ }
323
+
324
+ /**
325
+ * Stop periodic flushing and try to send remaining payloads.
326
+ * @returns {Promise<void>}
327
+ */
328
+ async close() {
329
+ if (this._timer) clearInterval(this._timer);
330
+ this._timer = null;
331
+ await this.flush();
332
+ }
333
+
334
+ /**
335
+ * Browser-only: capture window.onerror + unhandledrejection as error logs.
336
+ * @param {{includeSource?: boolean}=} options
337
+ */
338
+ captureBrowserErrors(options) {
339
+ if (!isBrowser()) return;
340
+ const includeSource = options?.includeSource ?? true;
341
+
342
+ window.addEventListener("error", (ev) => {
343
+ try {
344
+ const msg = ev.error?.message || ev.message || "window.error";
345
+ /** @type {Record<string, any>} */
346
+ const f = {
347
+ kind: "window.error",
348
+ stack: ev.error?.stack,
349
+ filename: includeSource ? ev.filename : undefined,
350
+ lineno: includeSource ? ev.lineno : undefined,
351
+ colno: includeSource ? ev.colno : undefined,
352
+ };
353
+ this.error(msg, f);
354
+ } catch {}
355
+ });
356
+
357
+ window.addEventListener("unhandledrejection", (ev) => {
358
+ try {
359
+ const reason = ev.reason;
360
+ const msg = reason?.message || String(reason || "unhandledrejection");
361
+ /** @type {Record<string, any>} */
362
+ const f = {
363
+ kind: "unhandledrejection",
364
+ reason: safeJson(reason) ?? String(reason),
365
+ stack: reason?.stack,
366
+ };
367
+ this.error(msg, f);
368
+ } catch {}
369
+ });
370
+ }
371
+
372
+ /**
373
+ * Node-only: capture process unhandledRejection + uncaughtException as error logs.
374
+ */
375
+ captureNodeErrors() {
376
+ if (isBrowser()) return;
377
+ const p = globalThis.process;
378
+ if (!p?.on) return;
379
+
380
+ p.on("unhandledRejection", (reason) => {
381
+ try {
382
+ const msg = reason?.message || String(reason || "unhandledRejection");
383
+ this.error(msg, { kind: "unhandledRejection", reason: safeJson(reason) ?? String(reason), stack: reason?.stack });
384
+ } catch {}
385
+ });
386
+
387
+ p.on("uncaughtException", (err) => {
388
+ try {
389
+ const msg = err?.message || String(err || "uncaughtException");
390
+ this.fatal(msg, { kind: "uncaughtException", stack: err?.stack });
391
+ } catch {}
392
+ });
393
+ }
394
+
395
+ _loadOrCreateDeviceId() {
396
+ if (isBrowser() && this._persistDeviceId) {
397
+ try {
398
+ const existing = localStorage.getItem("logtap_device_id");
399
+ if (existing && existing.trim()) return existing.trim();
400
+ } catch {}
401
+ const id = newDeviceId();
402
+ try {
403
+ localStorage.setItem("logtap_device_id", id);
404
+ } catch {}
405
+ return id;
406
+ }
407
+ return newDeviceId();
408
+ }
409
+
410
+ /** @param {LogtapLog} payload */
411
+ _enqueueLog(payload) {
412
+ const p = this._applyBeforeSend(payload);
413
+ if (!p) return;
414
+ this._logQueue.push(p);
415
+ if (this._logQueue.length > this._maxQueueSize) {
416
+ this._logQueue.splice(0, this._logQueue.length - this._maxQueueSize);
417
+ }
418
+ }
419
+
420
+ /** @param {LogtapTrackEvent} payload */
421
+ _enqueueTrack(payload) {
422
+ const p = this._applyBeforeSend(payload);
423
+ if (!p) return;
424
+ this._trackQueue.push(p);
425
+ if (this._trackQueue.length > this._maxQueueSize) {
426
+ this._trackQueue.splice(0, this._trackQueue.length - this._maxQueueSize);
427
+ }
428
+ }
429
+
430
+ _applyBeforeSend(payload) {
431
+ if (!this._beforeSend) return payload;
432
+ try {
433
+ return this._beforeSend(payload);
434
+ } catch {
435
+ return payload;
436
+ }
437
+ }
438
+
439
+ async _flushInner() {
440
+ if (this._backoffMs > 0) {
441
+ await sleep(this._backoffMs);
442
+ }
443
+
444
+ let sentAny = false;
445
+ let failed = false;
446
+
447
+ while (this._logQueue.length > 0) {
448
+ const ok = await this._flushQueue("/logs/", this._logQueue);
449
+ if (!ok) {
450
+ failed = true;
451
+ break;
452
+ }
453
+ sentAny = true;
454
+ }
455
+
456
+ while (this._trackQueue.length > 0) {
457
+ const ok = await this._flushQueue("/track/", this._trackQueue);
458
+ if (!ok) {
459
+ failed = true;
460
+ break;
461
+ }
462
+ sentAny = true;
463
+ }
464
+
465
+ if (sentAny && !failed) {
466
+ this._backoffMs = 0;
467
+ return;
468
+ }
469
+ }
470
+
471
+ async _flushQueue(path, queue) {
472
+ if (queue.length === 0) return false;
473
+
474
+ const batch = queue.splice(0, this._maxBatchSize);
475
+ if (batch.length === 0) return false;
476
+
477
+ const ok = await this._postJSON(path, batch);
478
+ if (!ok) {
479
+ queue.unshift(...batch);
480
+ this._backoffMs = this._backoffMs > 0 ? Math.min(this._backoffMs * 2, 30000) : 500;
481
+ return false;
482
+ }
483
+ return true;
484
+ }
485
+
486
+ async _postJSON(path, payload) {
487
+ if (typeof fetch !== "function") {
488
+ throw new Error("global fetch() is required (Node 18+ or provide a polyfill)");
489
+ }
490
+
491
+ const url = `${this._baseUrl}/api/${encodeURIComponent(this._projectId)}${path}`;
492
+ const json = JSON.stringify(payload);
493
+ const { body, contentEncoding } = await gzipIfEnabled(json, this._gzip);
494
+
495
+ /** @type {Record<string, string>} */
496
+ const headers = { "Content-Type": "application/json" };
497
+ if (this._projectKey) headers["X-Project-Key"] = this._projectKey;
498
+ if (contentEncoding) headers["Content-Encoding"] = contentEncoding;
499
+
500
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
501
+ const timeoutMs = this._timeoutMs > 0 ? this._timeoutMs : 0;
502
+ const timer = timeoutMs > 0 ? setTimeout(() => controller?.abort(), timeoutMs) : null;
503
+
504
+ try {
505
+ const res = await fetch(url, {
506
+ method: "POST",
507
+ headers,
508
+ body,
509
+ keepalive: isBrowser(),
510
+ signal: controller?.signal,
511
+ });
512
+ return res.status >= 200 && res.status < 300;
513
+ } catch {
514
+ return false;
515
+ } finally {
516
+ if (timer) clearTimeout(timer);
517
+ }
518
+ }
519
+ }
520
+
521
+ function toTimestamp(v) {
522
+ if (!v) return undefined;
523
+ if (typeof v === "string") return v;
524
+ if (v instanceof Date) return v.toISOString();
525
+ return undefined;
526
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "logtap-sdk",
3
+ "version": "0.1.0",
4
+ "description": "logtap browser/node logging + tracking SDK",
5
+ "keywords": [
6
+ "logtap",
7
+ "logging",
8
+ "tracking",
9
+ "analytics",
10
+ "sdk",
11
+ "browser",
12
+ "node"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/aak1247/logtap.git",
17
+ "directory": "sdks/js/logtap"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/aak1247/logtap/issues"
21
+ },
22
+ "homepage": "https://github.com/aak1247/logtap#readme",
23
+ "type": "module",
24
+ "main": "./index.js",
25
+ "types": "./index.d.ts",
26
+ "files": [
27
+ "README.md",
28
+ "index.d.ts",
29
+ "index.js"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./index.d.ts",
34
+ "default": "./index.js"
35
+ }
36
+ },
37
+ "sideEffects": false
38
+ }