@gunsole/core 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 push1kb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # @gunsole/core
2
+
3
+ Gunsole JavaScript/TypeScript SDK for browser and Node.js environments.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @gunsole/core
9
+ # or
10
+ npm install @gunsole/core
11
+ # or
12
+ yarn add @gunsole/core
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Basic Setup
18
+
19
+ ```typescript
20
+ import { createGunsoleClient } from "@gunsole/core";
21
+
22
+ const gunsole = createGunsoleClient({
23
+ projectId: "your-project-id",
24
+ apiKey: "your-api-key",
25
+ mode: "cloud", // or "desktop" | "local"
26
+ env: "production",
27
+ appName: "my-app",
28
+ appVersion: "1.0.0",
29
+ });
30
+ ```
31
+
32
+ ### Logging
33
+
34
+ ```typescript
35
+ // Simple log
36
+ gunsole.log({
37
+ level: "info",
38
+ bucket: "user_action",
39
+ message: "User clicked button",
40
+ });
41
+
42
+ // Log with context and tags
43
+ gunsole.log({
44
+ level: "error",
45
+ bucket: "api_error",
46
+ message: "Failed to fetch user data",
47
+ context: {
48
+ userId: "123",
49
+ endpoint: "/api/users",
50
+ statusCode: 500,
51
+ },
52
+ tags: {
53
+ feature: "user-management",
54
+ severity: "high",
55
+ },
56
+ });
57
+ ```
58
+
59
+ ### User Tracking
60
+
61
+ ```typescript
62
+ gunsole.setUser({
63
+ id: "user-123",
64
+ email: "user@example.com",
65
+ name: "John Doe",
66
+ traits: {
67
+ plan: "premium",
68
+ signupDate: "2024-01-01",
69
+ },
70
+ });
71
+ ```
72
+
73
+ ### Session Tracking
74
+
75
+ ```typescript
76
+ gunsole.setSessionId("session-abc-123");
77
+ ```
78
+
79
+ ### Global Error Handlers
80
+
81
+ ```typescript
82
+ // Attach automatic error tracking
83
+ gunsole.attachGlobalErrorHandlers();
84
+
85
+ // Detach when done
86
+ gunsole.detachGlobalErrorHandlers();
87
+ ```
88
+
89
+ ### Manual Flush
90
+
91
+ ```typescript
92
+ // Flush pending logs immediately
93
+ await gunsole.flush();
94
+ ```
95
+
96
+ ## Configuration
97
+
98
+ ### Modes
99
+
100
+ - `cloud`: Sends logs to `https://api.gunsole.com` (default for SaaS)
101
+ - `desktop`: Sends logs to `http://localhost:8787` (Gunsole Desktop app)
102
+ - `local`: Sends logs to `http://localhost:17655` (local development)
103
+
104
+ ### Options
105
+
106
+ - `projectId` (required): Your Gunsole project identifier
107
+ - `apiKey` (optional): Your API key (required for cloud mode)
108
+ - `mode` (required): Client mode (`"desktop" | "local" | "cloud"`)
109
+ - `endpoint` (optional): Custom endpoint URL (overrides mode default)
110
+ - `env` (optional): Environment name (e.g., "production", "staging")
111
+ - `appName` (optional): Application name
112
+ - `appVersion` (optional): Application version
113
+ - `defaultTags` (optional): Default tags applied to all logs
114
+ - `batchSize` (optional): Number of logs to batch before sending (default: 10)
115
+ - `flushInterval` (optional): Auto-flush interval in ms (default: 5000)
116
+
117
+ ### Cleanup
118
+
119
+ ```typescript
120
+ // Flush remaining logs and release resources
121
+ await gunsole.destroy();
122
+ ```
123
+
124
+ > **Node.js note:** The `attachGlobalErrorHandlers()` method registers an `uncaughtException` listener. Be aware that in Node.js, uncaught exceptions leave the process in an undefined state. The SDK captures the error for logging but does not re-throw or exit — you may want to add your own handler that calls `process.exit(1)` after flushing.
125
+
126
+ ## API Reference
127
+
128
+ See [SDK Reference](../../docs/sdk-reference.md) for full API documentation including typed buckets, tag schemas, and all configuration options.
129
+
130
+ ## Features
131
+
132
+ - ✅ Browser and Node.js support
133
+ - ✅ Automatic batching and flushing
134
+ - ✅ Retry logic with exponential backoff
135
+ - ✅ Never crashes the host application
136
+ - ✅ TypeScript support with full type definitions
137
+ - ✅ Tree-shakeable ESM and CJS builds
138
+ - ✅ Global error handler integration
139
+
140
+ ## Development
141
+
142
+ ```bash
143
+ # Install dependencies
144
+ pnpm install
145
+
146
+ # Build
147
+ pnpm build
148
+
149
+ # Test
150
+ pnpm test
151
+
152
+ # Lint
153
+ pnpm lint
154
+
155
+ # Type check
156
+ pnpm typecheck
157
+ ```
158
+
159
+ ## License
160
+
161
+ MIT
162
+
package/dist/index.cjs ADDED
@@ -0,0 +1,477 @@
1
+ 'use strict';
2
+
3
+ // src/buckets.ts
4
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
5
+ "log",
6
+ "info",
7
+ "debug",
8
+ "warn",
9
+ "error",
10
+ "setUser",
11
+ "setSessionId",
12
+ "flush",
13
+ "destroy",
14
+ "attachGlobalErrorHandlers",
15
+ "detachGlobalErrorHandlers"
16
+ ]);
17
+ function createBucketLogger(client, bucketName) {
18
+ const logAtLevel = (level, message, options) => {
19
+ client.log(level, {
20
+ ...options,
21
+ message,
22
+ bucket: bucketName
23
+ });
24
+ };
25
+ const logger = ((message, options) => {
26
+ logAtLevel("info", message, options);
27
+ });
28
+ logger.info = (message, options) => logAtLevel("info", message, options);
29
+ logger.debug = (message, options) => logAtLevel("debug", message, options);
30
+ logger.warn = (message, options) => logAtLevel("warn", message, options);
31
+ logger.error = (message, options) => logAtLevel("error", message, options);
32
+ return logger;
33
+ }
34
+ function attachBuckets(client, buckets) {
35
+ for (const name of buckets) {
36
+ if (RESERVED_NAMES.has(name)) {
37
+ throw new Error(
38
+ `Bucket name "${name}" conflicts with a reserved GunsoleClient method`
39
+ );
40
+ }
41
+ client[name] = createBucketLogger(client, name);
42
+ }
43
+ return client;
44
+ }
45
+
46
+ // src/config.ts
47
+ var DEFAULT_ENDPOINTS = {
48
+ desktop: "http://localhost:8787",
49
+ local: "http://localhost:17655",
50
+ cloud: "https://api.gunsole.com"
51
+ };
52
+ var DEFAULT_CONFIG = {
53
+ batchSize: 10,
54
+ flushInterval: 5e3
55
+ };
56
+ function resolveEndpoint(mode, customEndpoint) {
57
+ if (customEndpoint) {
58
+ return customEndpoint;
59
+ }
60
+ return DEFAULT_ENDPOINTS[mode];
61
+ }
62
+ function normalizeConfig(config) {
63
+ if (!config.projectId) {
64
+ throw new Error("projectId is required");
65
+ }
66
+ return {
67
+ projectId: config.projectId,
68
+ apiKey: config.apiKey ?? "",
69
+ mode: config.mode,
70
+ endpoint: resolveEndpoint(config.mode, config.endpoint),
71
+ env: config.env ?? "",
72
+ appName: config.appName ?? "",
73
+ appVersion: config.appVersion ?? "",
74
+ defaultTags: config.defaultTags ?? {},
75
+ batchSize: config.batchSize ?? DEFAULT_CONFIG.batchSize,
76
+ flushInterval: config.flushInterval ?? DEFAULT_CONFIG.flushInterval,
77
+ fetch: config.fetch,
78
+ isDebug: config.isDebug ?? false,
79
+ isDisabled: config.isDisabled ?? false,
80
+ buckets: config.buckets ?? []
81
+ };
82
+ }
83
+
84
+ // src/utils/env.ts
85
+ function isBrowser() {
86
+ return typeof window !== "undefined" && typeof window.document !== "undefined";
87
+ }
88
+ function isNode() {
89
+ return typeof process !== "undefined" && typeof process.versions !== "undefined" && typeof process.versions.node !== "undefined";
90
+ }
91
+ function getFetch(customFetch) {
92
+ if (isBrowser()) {
93
+ return window.fetch.bind(window);
94
+ }
95
+ if (isNode()) {
96
+ if (typeof globalThis.fetch !== "undefined") {
97
+ return globalThis.fetch;
98
+ }
99
+ throw new Error(
100
+ "fetch is not available. Please use Node.js 18+ or provide a custom fetch implementation in the config"
101
+ );
102
+ }
103
+ throw new Error("Unsupported environment: neither browser nor Node.js");
104
+ }
105
+
106
+ // src/transport.ts
107
+ var MAX_RETRIES = 3;
108
+ var BASE_DELAY_MS = 1e3;
109
+ function calculateBackoffDelay(attempt) {
110
+ return BASE_DELAY_MS * 2 ** attempt;
111
+ }
112
+ function sleep(ms) {
113
+ return new Promise((resolve) => setTimeout(resolve, ms));
114
+ }
115
+ async function gzipCompress(input) {
116
+ const encoder = new TextEncoder();
117
+ const stream = new Blob([encoder.encode(input)]).stream().pipeThrough(new CompressionStream("gzip"));
118
+ return new Uint8Array(await new Response(stream).arrayBuffer());
119
+ }
120
+ async function compressPayload(payload, isDebug) {
121
+ const minified = JSON.stringify(payload);
122
+ if (isDebug) {
123
+ return minified;
124
+ }
125
+ return gzipCompress(minified);
126
+ }
127
+ var Transport = class {
128
+ constructor(endpoint, apiKey, projectId, fetch, isDebug = false) {
129
+ this.endpoint = endpoint;
130
+ this.apiKey = apiKey;
131
+ this.projectId = projectId;
132
+ this.fetch = fetch ?? getFetch();
133
+ this.isDebug = isDebug;
134
+ }
135
+ /**
136
+ * Send a batch of logs to the API
137
+ * Implements retry logic with exponential backoff
138
+ */
139
+ async sendBatch(logs) {
140
+ if (logs.length === 0) {
141
+ return;
142
+ }
143
+ const payload = {
144
+ projectId: this.projectId,
145
+ logs
146
+ };
147
+ let lastError = null;
148
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
149
+ try {
150
+ const body = await compressPayload(payload, this.isDebug);
151
+ const headers = {
152
+ "Content-Type": "application/json"
153
+ };
154
+ if (this.apiKey) {
155
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
156
+ }
157
+ if (!this.isDebug) {
158
+ headers["Content-Encoding"] = "gzip";
159
+ }
160
+ const response = await this.fetch(`${this.endpoint}/logs`, {
161
+ method: "POST",
162
+ headers,
163
+ body
164
+ });
165
+ if (response.ok) {
166
+ return;
167
+ }
168
+ const errorText = await response.text().catch(() => "Unknown error");
169
+ lastError = new Error(`HTTP ${response.status}: ${errorText}`);
170
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
171
+ break;
172
+ }
173
+ } catch (error) {
174
+ lastError = error instanceof Error ? error : new Error(String(error));
175
+ }
176
+ if (attempt < MAX_RETRIES - 1) {
177
+ const delay = calculateBackoffDelay(attempt);
178
+ await sleep(delay);
179
+ }
180
+ }
181
+ if (process.env.NODE_ENV === "development") {
182
+ console.warn("[Gunsole] Failed to send logs after retries:", lastError);
183
+ }
184
+ }
185
+ };
186
+
187
+ // src/client.ts
188
+ var GunsoleClient = class {
189
+ constructor(config) {
190
+ this.batch = [];
191
+ this.flushTimer = null;
192
+ this.user = null;
193
+ this.sessionId = null;
194
+ this.globalHandlers = { attached: false };
195
+ this.config = normalizeConfig(config);
196
+ this.disabled = config.isDisabled ?? false;
197
+ this.transport = new Transport(
198
+ this.config.endpoint,
199
+ this.config.apiKey,
200
+ this.config.projectId,
201
+ this.config.fetch,
202
+ config.isDebug ?? false
203
+ );
204
+ if (this.disabled) {
205
+ return;
206
+ }
207
+ this.startFlushTimer();
208
+ }
209
+ log(levelOrOptions, maybeOptions) {
210
+ if (this.disabled) {
211
+ return;
212
+ }
213
+ const level = typeof levelOrOptions === "string" ? levelOrOptions : "info";
214
+ const options = typeof levelOrOptions === "string" ? maybeOptions : levelOrOptions;
215
+ try {
216
+ const internalEntry = {
217
+ level,
218
+ bucket: options.bucket,
219
+ message: options.message,
220
+ context: options.context,
221
+ timestamp: Date.now(),
222
+ traceId: options.traceId,
223
+ userId: this.user?.id,
224
+ sessionId: this.sessionId ?? void 0,
225
+ env: this.config.env || void 0,
226
+ appName: this.config.appName || void 0,
227
+ appVersion: this.config.appVersion || void 0,
228
+ tags: {
229
+ ...this.config.defaultTags,
230
+ ...Array.isArray(options.tags) ? Object.assign({}, ...options.tags) : options.tags
231
+ }
232
+ };
233
+ this.batch.push(internalEntry);
234
+ if (this.batch.length >= this.config.batchSize) {
235
+ this.flush();
236
+ }
237
+ } catch (error) {
238
+ if (process.env.NODE_ENV === "development") {
239
+ console.warn("[Gunsole] Error in log():", error);
240
+ }
241
+ }
242
+ }
243
+ /**
244
+ * Log an info-level message
245
+ */
246
+ info(options) {
247
+ this.log("info", options);
248
+ }
249
+ debug(options) {
250
+ this.log("debug", options);
251
+ }
252
+ warn(options) {
253
+ this.log("warn", options);
254
+ }
255
+ error(options) {
256
+ this.log("error", options);
257
+ }
258
+ /**
259
+ * Set user information
260
+ */
261
+ setUser(user) {
262
+ if (this.disabled) {
263
+ return;
264
+ }
265
+ try {
266
+ this.user = user;
267
+ } catch (error) {
268
+ if (process.env.NODE_ENV === "development") {
269
+ console.warn("[Gunsole] Error in setUser():", error);
270
+ }
271
+ }
272
+ }
273
+ /**
274
+ * Set session ID
275
+ */
276
+ setSessionId(sessionId) {
277
+ if (this.disabled) {
278
+ return;
279
+ }
280
+ try {
281
+ this.sessionId = sessionId;
282
+ } catch (error) {
283
+ if (process.env.NODE_ENV === "development") {
284
+ console.warn("[Gunsole] Error in setSessionId():", error);
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * Flush pending logs to the API
290
+ */
291
+ async flush() {
292
+ if (this.disabled) {
293
+ return;
294
+ }
295
+ try {
296
+ if (this.batch.length === 0) {
297
+ return;
298
+ }
299
+ const logsToSend = [...this.batch];
300
+ this.batch = [];
301
+ await this.transport.sendBatch(logsToSend);
302
+ } catch (error) {
303
+ if (process.env.NODE_ENV === "development") {
304
+ console.warn("[Gunsole] Error in flush():", error);
305
+ }
306
+ }
307
+ }
308
+ /**
309
+ * Attach global error handlers
310
+ */
311
+ attachGlobalErrorHandlers() {
312
+ if (this.disabled) {
313
+ return;
314
+ }
315
+ try {
316
+ if (this.globalHandlers.attached) {
317
+ return;
318
+ }
319
+ this.globalHandlers.unhandledRejection = (event) => {
320
+ this.error({
321
+ message: "Unhandled promise rejection",
322
+ bucket: "unhandled_rejection",
323
+ context: {
324
+ reason: String(event.reason),
325
+ error: event.reason instanceof Error ? {
326
+ name: event.reason.name,
327
+ message: event.reason.message,
328
+ stack: event.reason.stack
329
+ } : event.reason
330
+ }
331
+ });
332
+ };
333
+ this.globalHandlers.error = (event) => {
334
+ this.error({
335
+ message: event.message || "Global error",
336
+ bucket: "global_error",
337
+ context: {
338
+ filename: event.filename,
339
+ lineno: event.lineno,
340
+ colno: event.colno,
341
+ error: event.error ? {
342
+ name: event.error.name,
343
+ message: event.error.message,
344
+ stack: event.error.stack
345
+ } : void 0
346
+ }
347
+ });
348
+ };
349
+ if (typeof window !== "undefined") {
350
+ window.addEventListener(
351
+ "unhandledrejection",
352
+ this.globalHandlers.unhandledRejection
353
+ );
354
+ window.addEventListener("error", this.globalHandlers.error);
355
+ }
356
+ if (typeof process !== "undefined") {
357
+ this.globalHandlers.unhandledRejectionNode = (reason, _promise) => {
358
+ this.error({
359
+ message: "Unhandled promise rejection",
360
+ bucket: "unhandled_rejection",
361
+ context: {
362
+ reason: String(reason),
363
+ error: reason instanceof Error ? {
364
+ name: reason.name,
365
+ message: reason.message,
366
+ stack: reason.stack
367
+ } : reason
368
+ }
369
+ });
370
+ };
371
+ this.globalHandlers.uncaughtException = (error) => {
372
+ this.error({
373
+ message: error.message,
374
+ bucket: "uncaught_exception",
375
+ context: {
376
+ name: error.name,
377
+ stack: error.stack
378
+ }
379
+ });
380
+ };
381
+ process.on(
382
+ "unhandledRejection",
383
+ this.globalHandlers.unhandledRejectionNode
384
+ );
385
+ process.on("uncaughtException", this.globalHandlers.uncaughtException);
386
+ }
387
+ this.globalHandlers.attached = true;
388
+ } catch (error) {
389
+ if (process.env.NODE_ENV === "development") {
390
+ console.warn("[Gunsole] Error in attachGlobalErrorHandlers():", error);
391
+ }
392
+ }
393
+ }
394
+ /**
395
+ * Detach global error handlers
396
+ */
397
+ detachGlobalErrorHandlers() {
398
+ try {
399
+ if (!this.globalHandlers.attached) {
400
+ return;
401
+ }
402
+ if (typeof window !== "undefined") {
403
+ if (this.globalHandlers.unhandledRejection) {
404
+ window.removeEventListener(
405
+ "unhandledrejection",
406
+ this.globalHandlers.unhandledRejection
407
+ );
408
+ }
409
+ if (this.globalHandlers.error) {
410
+ window.removeEventListener("error", this.globalHandlers.error);
411
+ }
412
+ }
413
+ if (typeof process !== "undefined") {
414
+ if (this.globalHandlers.unhandledRejectionNode) {
415
+ process.removeListener(
416
+ "unhandledRejection",
417
+ this.globalHandlers.unhandledRejectionNode
418
+ );
419
+ }
420
+ if (this.globalHandlers.uncaughtException) {
421
+ process.removeListener(
422
+ "uncaughtException",
423
+ this.globalHandlers.uncaughtException
424
+ );
425
+ }
426
+ }
427
+ this.globalHandlers = { attached: false };
428
+ } catch (error) {
429
+ if (process.env.NODE_ENV === "development") {
430
+ console.warn("[Gunsole] Error in detachGlobalErrorHandlers():", error);
431
+ }
432
+ }
433
+ }
434
+ /**
435
+ * Start the automatic flush timer
436
+ */
437
+ startFlushTimer() {
438
+ if (this.flushTimer) {
439
+ return;
440
+ }
441
+ this.flushTimer = setInterval(() => {
442
+ this.flush();
443
+ }, this.config.flushInterval);
444
+ }
445
+ /**
446
+ * Stop the automatic flush timer
447
+ */
448
+ stopFlushTimer() {
449
+ if (this.flushTimer) {
450
+ clearInterval(this.flushTimer);
451
+ this.flushTimer = null;
452
+ }
453
+ }
454
+ /**
455
+ * Cleanup resources. Awaiting ensures remaining logs are flushed.
456
+ */
457
+ async destroy() {
458
+ this.stopFlushTimer();
459
+ this.detachGlobalErrorHandlers();
460
+ await this.flush();
461
+ }
462
+ };
463
+
464
+ // src/factory.ts
465
+ function createGunsoleClient(config) {
466
+ const client = new GunsoleClient(config);
467
+ const buckets = config.buckets ?? [];
468
+ if (buckets.length > 0) {
469
+ return attachBuckets(client, buckets);
470
+ }
471
+ return client;
472
+ }
473
+
474
+ exports.GunsoleClient = GunsoleClient;
475
+ exports.createGunsoleClient = createGunsoleClient;
476
+ //# sourceMappingURL=index.cjs.map
477
+ //# sourceMappingURL=index.cjs.map