logstack-js 1.0.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) 2026 Mosesedem
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,77 @@
1
+ # logstack-js
2
+
3
+ The JavaScript/TypeScript SDK for [Logstack](https://github.com/Mosesedem/logstack) — log
4
+ ingestion, real-time streaming, and querying.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm install logstack-js
10
+ # or: pnpm add logstack-js / yarn add logstack-js
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { createLogStack } from "logstack-js";
17
+
18
+ const logstack = createLogStack({
19
+ apiKey: "ls_live_xxx",
20
+ // endpoint host (SDK appends /v1/logs); defaults to https://api.logstack.tech
21
+ // endpoint: "http://localhost:8080",
22
+ });
23
+
24
+ logstack.info("Application started", { version: "1.0.0" });
25
+ logstack.error("Payment failed", { orderId: "ord_123", code: 402 });
26
+
27
+ // flush + stop timers on shutdown
28
+ await logstack.close();
29
+ ```
30
+
31
+ ## Behavior
32
+
33
+ - **Console and server are independent.** Every log is written to the console (always in
34
+ development/test; in production only when `consoleInProduction: true`, unless `silent`),
35
+ and is *also* shipped to the server whenever an `apiKey` is set and `disabled` is false —
36
+ in **all** environments.
37
+ - A missing API key should degrade the client to console-only (`disabled: true`); it must
38
+ never become a silent no-op.
39
+ - Browser logs are queued to `localStorage` while offline (bounded by `maxOfflineQueueSize`,
40
+ default 1000) and flushed on reconnect.
41
+
42
+ ## Configuration
43
+
44
+ | Option | Default | Description |
45
+ | ------ | ------- | ----------- |
46
+ | `apiKey` | — | **Required.** Project API key (`ls_...`). |
47
+ | `endpoint` | `https://api.logstack.tech` | API host; the SDK appends `/v1/logs`. |
48
+ | `environment` | auto (`NODE_ENV`) | `development` \| `staging` \| `production` \| `test`. |
49
+ | `batchSize` | `100` | Logs buffered before an auto-flush. |
50
+ | `flushInterval` | `5000` | Auto-flush interval (ms). |
51
+ | `maxRetries` | `3` | Retry attempts for `5xx` responses. |
52
+ | `consoleInProduction` | `false` | Also log to console in production/staging. |
53
+ | `silent` | `false` | Disable all console output. |
54
+ | `disabled` | `false` | Console-only mode: never buffer, send, or queue. |
55
+ | `maxOfflineQueueSize` | `1000` | Cap on the offline (localStorage) queue. |
56
+ | `captureContext` | `true` | Auto-capture URL/route/user-agent in the browser. |
57
+ | `onError` | — | `(error, logs) => void` callback for send failures. |
58
+
59
+ ## Log levels
60
+
61
+ `debug` · `info` · `warn` · `error` · `critical` · `fatal` — each available as a method, e.g.
62
+ `logstack.debug(message, metadata?)`.
63
+
64
+ ## Querying
65
+
66
+ With `projectId` in the config you can read logs back:
67
+
68
+ ```ts
69
+ const result = await logstack.getLogs({ level: "error", limit: 50 });
70
+ const one = await logstack.getLogById(12345);
71
+ ```
72
+
73
+ Full reference: [docs/SDK.md](https://github.com/Mosesedem/logstack/blob/main/docs/SDK.md).
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,132 @@
1
+ type LogLevel = "debug" | "info" | "warn" | "error" | "critical" | "fatal";
2
+ type Environment = "development" | "production" | "staging" | "test";
3
+ interface LogContext {
4
+ /** Page URL or API endpoint where the log originated */
5
+ url?: string;
6
+ /** Route path (for framework routing) */
7
+ route?: string;
8
+ /** HTTP method (for API endpoints) */
9
+ method?: string;
10
+ /** User agent string */
11
+ userAgent?: string;
12
+ /** Client IP address */
13
+ ip?: string;
14
+ /** Request ID for tracing */
15
+ requestId?: string;
16
+ /** Component or module name */
17
+ component?: string;
18
+ }
19
+ interface LogEntry {
20
+ level: LogLevel;
21
+ message: string;
22
+ source?: string;
23
+ metadata?: Record<string, unknown>;
24
+ timestamp?: string;
25
+ /** Contextual information about where the log originated */
26
+ context?: LogContext;
27
+ }
28
+ interface LogRecord extends LogEntry {
29
+ id: number;
30
+ projectId: string;
31
+ createdAt: string;
32
+ }
33
+ interface LogQueryOptions {
34
+ /** Filter by log level */
35
+ level?: LogLevel;
36
+ /** Search in message content */
37
+ search?: string;
38
+ /** Start time for date range filter (ISO 8601) */
39
+ startTime?: string;
40
+ /** End time for date range filter (ISO 8601) */
41
+ endTime?: string;
42
+ /** Number of records to skip */
43
+ offset?: number;
44
+ /** Maximum number of records to return (max 1000) */
45
+ limit?: number;
46
+ }
47
+ interface LogQueryResult {
48
+ logs: LogRecord[];
49
+ total: number;
50
+ offset: number;
51
+ limit: number;
52
+ }
53
+ interface LogStackConfig {
54
+ apiKey: string;
55
+ endpoint?: string;
56
+ batchSize?: number;
57
+ flushInterval?: number;
58
+ maxRetries?: number;
59
+ onError?: (error: Error, logs: LogEntry[]) => void;
60
+ /**
61
+ * Environment label attached to logs and used for console gating (see
62
+ * `consoleInProduction`). Logs are sent to the server in every environment as
63
+ * long as an `apiKey` is set and `disabled` is not true. Auto-detected from
64
+ * `NODE_ENV` when omitted; defaults to 'production'.
65
+ */
66
+ environment?: Environment;
67
+ /**
68
+ * Whether to log to the console in production/staging mode. In development and
69
+ * test, console output is always on (unless `silent`). Defaults to false, so
70
+ * production stays quiet on the console while still shipping logs to the server.
71
+ */
72
+ consoleInProduction?: boolean;
73
+ /**
74
+ * Disable all console output regardless of environment. Defaults to false.
75
+ */
76
+ silent?: boolean;
77
+ /**
78
+ * Console-only mode: log to the console but never buffer, send, or queue to the
79
+ * server. Use when no API key/endpoint is available (e.g. local dev without a
80
+ * configured project). Defaults to false.
81
+ */
82
+ disabled?: boolean;
83
+ /**
84
+ * Maximum number of logs retained in the offline (localStorage) queue. Oldest
85
+ * entries are dropped past this cap to avoid exceeding storage quota.
86
+ * Defaults to 1000.
87
+ */
88
+ maxOfflineQueueSize?: number;
89
+ /**
90
+ * Auto-capture context like URL, route, user agent.
91
+ * Defaults to true.
92
+ */
93
+ captureContext?: boolean;
94
+ /**
95
+ * Project ID for query operations. Required for getLogs/getLogById.
96
+ */
97
+ projectId?: string;
98
+ }
99
+ interface LogStackClient {
100
+ debug(message: string, metadata?: Record<string, unknown>): void;
101
+ info(message: string, metadata?: Record<string, unknown>): void;
102
+ warn(message: string, metadata?: Record<string, unknown>): void;
103
+ error(message: string, metadata?: Record<string, unknown>): void;
104
+ critical(message: string, metadata?: Record<string, unknown>): void;
105
+ fatal(message: string, metadata?: Record<string, unknown>): void;
106
+ log(entry: LogEntry): void;
107
+ flush(): Promise<void>;
108
+ close(): Promise<void>;
109
+ /** Set context that will be included with all subsequent logs */
110
+ setContext(context: LogContext): void;
111
+ /** Clear the current context */
112
+ clearContext(): void;
113
+ /** Query logs from the server (requires projectId in config) */
114
+ getLogs(options?: LogQueryOptions): Promise<LogQueryResult>;
115
+ /** Get a single log by ID (requires projectId in config) */
116
+ getLogById(logId: number): Promise<LogRecord>;
117
+ }
118
+ /** Structured error from Logstack API */
119
+ interface LogStackErrorResponse {
120
+ code: string;
121
+ message: string;
122
+ }
123
+ /** Error class for Logstack API errors */
124
+ declare class LogStackError extends Error {
125
+ code: string;
126
+ status: number;
127
+ constructor(code: string, message: string, status: number);
128
+ }
129
+
130
+ declare function createLogStack(config: LogStackConfig): LogStackClient;
131
+
132
+ export { type Environment, type LogContext, type LogEntry, type LogLevel, type LogQueryOptions, type LogQueryResult, type LogRecord, type LogStackClient, type LogStackConfig, LogStackError, type LogStackErrorResponse, createLogStack };
@@ -0,0 +1,132 @@
1
+ type LogLevel = "debug" | "info" | "warn" | "error" | "critical" | "fatal";
2
+ type Environment = "development" | "production" | "staging" | "test";
3
+ interface LogContext {
4
+ /** Page URL or API endpoint where the log originated */
5
+ url?: string;
6
+ /** Route path (for framework routing) */
7
+ route?: string;
8
+ /** HTTP method (for API endpoints) */
9
+ method?: string;
10
+ /** User agent string */
11
+ userAgent?: string;
12
+ /** Client IP address */
13
+ ip?: string;
14
+ /** Request ID for tracing */
15
+ requestId?: string;
16
+ /** Component or module name */
17
+ component?: string;
18
+ }
19
+ interface LogEntry {
20
+ level: LogLevel;
21
+ message: string;
22
+ source?: string;
23
+ metadata?: Record<string, unknown>;
24
+ timestamp?: string;
25
+ /** Contextual information about where the log originated */
26
+ context?: LogContext;
27
+ }
28
+ interface LogRecord extends LogEntry {
29
+ id: number;
30
+ projectId: string;
31
+ createdAt: string;
32
+ }
33
+ interface LogQueryOptions {
34
+ /** Filter by log level */
35
+ level?: LogLevel;
36
+ /** Search in message content */
37
+ search?: string;
38
+ /** Start time for date range filter (ISO 8601) */
39
+ startTime?: string;
40
+ /** End time for date range filter (ISO 8601) */
41
+ endTime?: string;
42
+ /** Number of records to skip */
43
+ offset?: number;
44
+ /** Maximum number of records to return (max 1000) */
45
+ limit?: number;
46
+ }
47
+ interface LogQueryResult {
48
+ logs: LogRecord[];
49
+ total: number;
50
+ offset: number;
51
+ limit: number;
52
+ }
53
+ interface LogStackConfig {
54
+ apiKey: string;
55
+ endpoint?: string;
56
+ batchSize?: number;
57
+ flushInterval?: number;
58
+ maxRetries?: number;
59
+ onError?: (error: Error, logs: LogEntry[]) => void;
60
+ /**
61
+ * Environment label attached to logs and used for console gating (see
62
+ * `consoleInProduction`). Logs are sent to the server in every environment as
63
+ * long as an `apiKey` is set and `disabled` is not true. Auto-detected from
64
+ * `NODE_ENV` when omitted; defaults to 'production'.
65
+ */
66
+ environment?: Environment;
67
+ /**
68
+ * Whether to log to the console in production/staging mode. In development and
69
+ * test, console output is always on (unless `silent`). Defaults to false, so
70
+ * production stays quiet on the console while still shipping logs to the server.
71
+ */
72
+ consoleInProduction?: boolean;
73
+ /**
74
+ * Disable all console output regardless of environment. Defaults to false.
75
+ */
76
+ silent?: boolean;
77
+ /**
78
+ * Console-only mode: log to the console but never buffer, send, or queue to the
79
+ * server. Use when no API key/endpoint is available (e.g. local dev without a
80
+ * configured project). Defaults to false.
81
+ */
82
+ disabled?: boolean;
83
+ /**
84
+ * Maximum number of logs retained in the offline (localStorage) queue. Oldest
85
+ * entries are dropped past this cap to avoid exceeding storage quota.
86
+ * Defaults to 1000.
87
+ */
88
+ maxOfflineQueueSize?: number;
89
+ /**
90
+ * Auto-capture context like URL, route, user agent.
91
+ * Defaults to true.
92
+ */
93
+ captureContext?: boolean;
94
+ /**
95
+ * Project ID for query operations. Required for getLogs/getLogById.
96
+ */
97
+ projectId?: string;
98
+ }
99
+ interface LogStackClient {
100
+ debug(message: string, metadata?: Record<string, unknown>): void;
101
+ info(message: string, metadata?: Record<string, unknown>): void;
102
+ warn(message: string, metadata?: Record<string, unknown>): void;
103
+ error(message: string, metadata?: Record<string, unknown>): void;
104
+ critical(message: string, metadata?: Record<string, unknown>): void;
105
+ fatal(message: string, metadata?: Record<string, unknown>): void;
106
+ log(entry: LogEntry): void;
107
+ flush(): Promise<void>;
108
+ close(): Promise<void>;
109
+ /** Set context that will be included with all subsequent logs */
110
+ setContext(context: LogContext): void;
111
+ /** Clear the current context */
112
+ clearContext(): void;
113
+ /** Query logs from the server (requires projectId in config) */
114
+ getLogs(options?: LogQueryOptions): Promise<LogQueryResult>;
115
+ /** Get a single log by ID (requires projectId in config) */
116
+ getLogById(logId: number): Promise<LogRecord>;
117
+ }
118
+ /** Structured error from Logstack API */
119
+ interface LogStackErrorResponse {
120
+ code: string;
121
+ message: string;
122
+ }
123
+ /** Error class for Logstack API errors */
124
+ declare class LogStackError extends Error {
125
+ code: string;
126
+ status: number;
127
+ constructor(code: string, message: string, status: number);
128
+ }
129
+
130
+ declare function createLogStack(config: LogStackConfig): LogStackClient;
131
+
132
+ export { type Environment, type LogContext, type LogEntry, type LogLevel, type LogQueryOptions, type LogQueryResult, type LogRecord, type LogStackClient, type LogStackConfig, LogStackError, type LogStackErrorResponse, createLogStack };
package/dist/index.js ADDED
@@ -0,0 +1,505 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ LogStackError: () => LogStackError,
24
+ createLogStack: () => createLogStack
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/types.ts
29
+ var LogStackError = class extends Error {
30
+ constructor(code, message, status) {
31
+ super(message);
32
+ this.code = code;
33
+ this.status = status;
34
+ this.name = "LogStackError";
35
+ }
36
+ };
37
+
38
+ // src/index.ts
39
+ var DEFAULT_ENDPOINT = "https://api.logstack.tech";
40
+ var DEFAULT_BATCH_SIZE = 100;
41
+ var DEFAULT_FLUSH_INTERVAL = 5e3;
42
+ var DEFAULT_MAX_RETRIES = 3;
43
+ var DEFAULT_MAX_OFFLINE_QUEUE = 1e3;
44
+ var RETRY_BASE_DELAY = 1e3;
45
+ var OFFLINE_STORAGE_KEY = "logstack_offline_queue";
46
+ var OFFLINE_STORAGE_TTL = 24 * 60 * 60 * 1e3;
47
+ var LEVEL_COLORS = {
48
+ debug: "\x1B[35m",
49
+ // Magenta
50
+ info: "\x1B[36m",
51
+ // Cyan
52
+ warn: "\x1B[33m",
53
+ // Yellow
54
+ error: "\x1B[31m",
55
+ // Red
56
+ critical: "\x1B[41m\x1B[37m",
57
+ // White on Red background
58
+ fatal: "\x1B[45m\x1B[37m"
59
+ // White on Magenta background
60
+ };
61
+ var RESET_COLOR = "\x1B[0m";
62
+ var BROWSER_STYLES = {
63
+ debug: "color: #a371f7; font-weight: bold",
64
+ info: "color: #58a6ff; font-weight: bold",
65
+ warn: "color: #d29922; font-weight: bold",
66
+ error: "color: #f85149; font-weight: bold",
67
+ critical: "background: #da3633; color: white; font-weight: bold; padding: 2px 6px; border-radius: 2px",
68
+ fatal: "background: #8957e5; color: white; font-weight: bold; padding: 2px 6px; border-radius: 2px"
69
+ };
70
+ function isBrowserEnvironment() {
71
+ return typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
72
+ }
73
+ function getBrowserWindow() {
74
+ if (!isBrowserEnvironment()) {
75
+ return void 0;
76
+ }
77
+ const win = globalThis.window;
78
+ if (typeof win !== "object" || win === null) {
79
+ return void 0;
80
+ }
81
+ return win;
82
+ }
83
+ function getBrowserStorage() {
84
+ if (!isBrowserEnvironment()) {
85
+ return void 0;
86
+ }
87
+ const storage = globalThis.localStorage;
88
+ if (typeof storage !== "object" || storage === null) {
89
+ return void 0;
90
+ }
91
+ return storage;
92
+ }
93
+ function getBrowserContext() {
94
+ const win = getBrowserWindow();
95
+ if (!win) {
96
+ return {};
97
+ }
98
+ return {
99
+ url: win.location?.href,
100
+ route: win.location?.pathname,
101
+ userAgent: win.navigator?.userAgent
102
+ };
103
+ }
104
+ var LogStack = class {
105
+ constructor(config) {
106
+ this.buffer = [];
107
+ this.flushTimer = null;
108
+ this.isFlushing = false;
109
+ this.isClosing = false;
110
+ this.globalContext = {};
111
+ this.isOnline = true;
112
+ this.offlineQueue = [];
113
+ this.offlineRetryTimer = null;
114
+ this.isBrowser = isBrowserEnvironment();
115
+ const detectedEnvironment = this.detectEnvironment();
116
+ this.config = {
117
+ apiKey: config.apiKey,
118
+ endpoint: config.endpoint || DEFAULT_ENDPOINT,
119
+ batchSize: config.batchSize || DEFAULT_BATCH_SIZE,
120
+ flushInterval: config.flushInterval || DEFAULT_FLUSH_INTERVAL,
121
+ maxRetries: config.maxRetries || DEFAULT_MAX_RETRIES,
122
+ environment: config.environment || detectedEnvironment,
123
+ consoleInProduction: config.consoleInProduction || false,
124
+ silent: config.silent || false,
125
+ disabled: config.disabled || false,
126
+ maxOfflineQueueSize: config.maxOfflineQueueSize ?? DEFAULT_MAX_OFFLINE_QUEUE,
127
+ captureContext: config.captureContext !== false,
128
+ onError: config.onError,
129
+ projectId: config.projectId
130
+ };
131
+ if (!this.config.disabled) {
132
+ this.startFlushTimer();
133
+ }
134
+ if (this.config.captureContext && this.isBrowser) {
135
+ this.captureDefaultContext();
136
+ }
137
+ if (this.isBrowser && !this.config.disabled) {
138
+ this.setupOfflineDetection();
139
+ this.restoreOfflineQueue();
140
+ }
141
+ }
142
+ detectEnvironment() {
143
+ if (typeof process !== "undefined" && process.env) {
144
+ const nodeEnv = process.env.NODE_ENV;
145
+ if (nodeEnv === "development" || nodeEnv === "dev") return "development";
146
+ if (nodeEnv === "test") return "test";
147
+ if (nodeEnv === "staging") return "staging";
148
+ }
149
+ return "production";
150
+ }
151
+ setupOfflineDetection() {
152
+ const win = getBrowserWindow();
153
+ if (!win) return;
154
+ this.isOnline = win.navigator?.onLine ?? true;
155
+ win.addEventListener?.("online", () => {
156
+ this.isOnline = true;
157
+ this.flushOfflineQueue().catch(() => {
158
+ });
159
+ });
160
+ win.addEventListener?.("offline", () => {
161
+ this.isOnline = false;
162
+ });
163
+ }
164
+ async flushOfflineQueue() {
165
+ if (this.offlineQueue.length === 0) {
166
+ return;
167
+ }
168
+ const logsToSend = [...this.offlineQueue];
169
+ this.offlineQueue = [];
170
+ this.saveOfflineQueue();
171
+ try {
172
+ await this.sendLogs(logsToSend);
173
+ } catch (error) {
174
+ this.offlineQueue = [...logsToSend, ...this.offlineQueue];
175
+ this.saveOfflineQueue();
176
+ throw error;
177
+ }
178
+ }
179
+ saveOfflineQueue() {
180
+ if (!this.isBrowser) return;
181
+ const storage = getBrowserStorage();
182
+ if (!storage) return;
183
+ try {
184
+ const dataToStore = this.offlineQueue.map((log) => ({
185
+ ...log,
186
+ timestamp: log.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
187
+ savedAt: (/* @__PURE__ */ new Date()).getTime()
188
+ }));
189
+ storage.setItem(OFFLINE_STORAGE_KEY, JSON.stringify(dataToStore));
190
+ } catch (error) {
191
+ if (this.config.onError && error instanceof Error) {
192
+ this.config.onError(error, []);
193
+ }
194
+ }
195
+ }
196
+ restoreOfflineQueue() {
197
+ if (!this.isBrowser) return;
198
+ const storage = getBrowserStorage();
199
+ if (!storage) return;
200
+ try {
201
+ const stored = storage.getItem(OFFLINE_STORAGE_KEY);
202
+ if (stored) {
203
+ const logs = JSON.parse(stored);
204
+ const now = (/* @__PURE__ */ new Date()).getTime();
205
+ this.offlineQueue = logs.filter((log) => now - log.savedAt < OFFLINE_STORAGE_TTL).map(({ savedAt, ...log }) => log);
206
+ this.saveOfflineQueue();
207
+ }
208
+ } catch (error) {
209
+ storage.removeItem(OFFLINE_STORAGE_KEY);
210
+ }
211
+ }
212
+ isProductionMode() {
213
+ return this.config.environment === "production" || this.config.environment === "staging";
214
+ }
215
+ /**
216
+ * Whether this log should be written to the console. Always on in
217
+ * development/test; in production/staging only when consoleInProduction is set.
218
+ * `silent` disables console output entirely.
219
+ */
220
+ shouldLogToConsole() {
221
+ if (this.config.silent) {
222
+ return false;
223
+ }
224
+ if (this.isProductionMode()) {
225
+ return this.config.consoleInProduction;
226
+ }
227
+ return true;
228
+ }
229
+ /**
230
+ * Append logs to the offline queue, dropping the oldest entries past the
231
+ * configured cap to avoid exceeding localStorage quota.
232
+ */
233
+ enqueueOffline(logs) {
234
+ this.offlineQueue.push(...logs);
235
+ const max = this.config.maxOfflineQueueSize;
236
+ if (this.offlineQueue.length > max) {
237
+ this.offlineQueue = this.offlineQueue.slice(-max);
238
+ }
239
+ this.saveOfflineQueue();
240
+ }
241
+ captureDefaultContext() {
242
+ this.globalContext = getBrowserContext();
243
+ }
244
+ formatTimestamp(timestamp) {
245
+ const date = new Date(timestamp);
246
+ return date.toISOString().replace("T", " ").slice(0, 23);
247
+ }
248
+ logToConsole(entry) {
249
+ const timestamp = this.formatTimestamp(
250
+ entry.timestamp || (/* @__PURE__ */ new Date()).toISOString()
251
+ );
252
+ const level = entry.level.toUpperCase().padEnd(8);
253
+ if (this.isBrowser) {
254
+ const style = BROWSER_STYLES[entry.level];
255
+ const contextStr = entry.context?.url ? ` [${entry.context.url}]` : "";
256
+ console.log(
257
+ `%c${level}%c ${timestamp}${contextStr} - ${entry.message}`,
258
+ style,
259
+ "color: inherit",
260
+ entry.metadata || ""
261
+ );
262
+ } else {
263
+ const color = LEVEL_COLORS[entry.level];
264
+ const contextStr = entry.context?.url ? ` [${entry.context.url}]` : entry.context?.route ? ` [${entry.context.route}]` : "";
265
+ const metaStr = entry.metadata ? ` ${JSON.stringify(entry.metadata)}` : "";
266
+ console.log(
267
+ `${color}${level}${RESET_COLOR} ${timestamp}${contextStr} - ${entry.message}${metaStr}`
268
+ );
269
+ }
270
+ }
271
+ startFlushTimer() {
272
+ this.flushTimer = setInterval(() => {
273
+ if (this.buffer.length > 0 && !this.isFlushing) {
274
+ this.flush().catch(() => {
275
+ });
276
+ }
277
+ }, this.config.flushInterval);
278
+ }
279
+ stopFlushTimer() {
280
+ if (this.flushTimer) {
281
+ clearInterval(this.flushTimer);
282
+ this.flushTimer = null;
283
+ }
284
+ }
285
+ setContext(context) {
286
+ this.globalContext = { ...this.globalContext, ...context };
287
+ }
288
+ clearContext() {
289
+ this.globalContext = {};
290
+ if (this.config.captureContext && this.isBrowser) {
291
+ this.captureDefaultContext();
292
+ }
293
+ }
294
+ debug(message, metadata) {
295
+ this.log({ level: "debug", message, metadata });
296
+ }
297
+ info(message, metadata) {
298
+ this.log({ level: "info", message, metadata });
299
+ }
300
+ warn(message, metadata) {
301
+ this.log({ level: "warn", message, metadata });
302
+ }
303
+ error(message, metadata) {
304
+ this.log({ level: "error", message, metadata });
305
+ }
306
+ critical(message, metadata) {
307
+ this.log({ level: "critical", message, metadata });
308
+ }
309
+ fatal(message, metadata) {
310
+ this.log({ level: "fatal", message, metadata });
311
+ }
312
+ log(entry) {
313
+ if (this.isClosing) {
314
+ console.warn("Logstack: Cannot log after close() has been called");
315
+ return;
316
+ }
317
+ const timestamp = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
318
+ const context = {
319
+ ...this.globalContext,
320
+ ...entry.context
321
+ };
322
+ const logEntry = {
323
+ ...entry,
324
+ timestamp,
325
+ context: Object.keys(context).length > 0 ? context : void 0
326
+ };
327
+ if (this.shouldLogToConsole()) {
328
+ this.logToConsole(logEntry);
329
+ }
330
+ if (this.config.disabled) {
331
+ return;
332
+ }
333
+ if (!this.isOnline) {
334
+ this.enqueueOffline([logEntry]);
335
+ return;
336
+ }
337
+ this.buffer.push(logEntry);
338
+ if (this.buffer.length >= this.config.batchSize && !this.isFlushing) {
339
+ this.flush().catch(() => {
340
+ });
341
+ }
342
+ }
343
+ async flush() {
344
+ if (this.config.disabled) {
345
+ return;
346
+ }
347
+ if (this.buffer.length === 0 || this.isFlushing) {
348
+ return;
349
+ }
350
+ this.isFlushing = true;
351
+ const logsToSend = [...this.buffer];
352
+ this.buffer = [];
353
+ try {
354
+ await this.sendLogs(logsToSend);
355
+ } catch (error) {
356
+ this.buffer = [...logsToSend, ...this.buffer];
357
+ if (this.config.onError && error instanceof Error) {
358
+ this.config.onError(error, logsToSend);
359
+ }
360
+ throw error;
361
+ } finally {
362
+ this.isFlushing = false;
363
+ }
364
+ }
365
+ async sendLogs(logs, attempt = 1) {
366
+ try {
367
+ const response = await fetch(`${this.config.endpoint}/v1/logs`, {
368
+ method: "POST",
369
+ headers: {
370
+ "Content-Type": "application/json",
371
+ Authorization: `Bearer ${this.config.apiKey}`
372
+ },
373
+ body: JSON.stringify({ logs })
374
+ });
375
+ if (!response.ok) {
376
+ if (response.status >= 500 && attempt < this.config.maxRetries) {
377
+ const delay = RETRY_BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 100;
378
+ await new Promise((resolve) => setTimeout(resolve, delay));
379
+ return this.sendLogs(logs, attempt + 1);
380
+ }
381
+ let errorData = {
382
+ code: "SEND_ERROR",
383
+ message: `Failed to send logs: ${response.status}`
384
+ };
385
+ try {
386
+ const data = await response.json();
387
+ if (data && data.code && data.message) {
388
+ errorData = data;
389
+ }
390
+ } catch {
391
+ }
392
+ throw new LogStackError(
393
+ errorData.code,
394
+ errorData.message,
395
+ response.status
396
+ );
397
+ }
398
+ } catch (error) {
399
+ if (this.isOnline && error instanceof Error) {
400
+ this.enqueueOffline(logs);
401
+ this.isOnline = false;
402
+ throw error;
403
+ }
404
+ throw error;
405
+ }
406
+ }
407
+ async getLogs(options = {}) {
408
+ if (!this.config.projectId) {
409
+ throw new LogStackError(
410
+ "MISSING_PROJECT_ID",
411
+ "projectId is required in config for query operations",
412
+ 400
413
+ );
414
+ }
415
+ const params = new URLSearchParams();
416
+ params.set("projectId", this.config.projectId);
417
+ if (options.level) params.set("level", options.level);
418
+ if (options.search) params.set("search", options.search);
419
+ if (options.startTime) params.set("startTime", options.startTime);
420
+ if (options.endTime) params.set("endTime", options.endTime);
421
+ if (options.offset !== void 0)
422
+ params.set("offset", String(options.offset));
423
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
424
+ const response = await this.fetchWithRetry(
425
+ `${this.config.endpoint}/v1/logs?${params.toString()}`,
426
+ { method: "GET" }
427
+ );
428
+ return await response.json();
429
+ }
430
+ async getLogById(logId) {
431
+ if (!this.config.projectId) {
432
+ throw new LogStackError(
433
+ "MISSING_PROJECT_ID",
434
+ "projectId is required in config for query operations",
435
+ 400
436
+ );
437
+ }
438
+ const params = new URLSearchParams();
439
+ params.set("projectId", this.config.projectId);
440
+ const response = await this.fetchWithRetry(
441
+ `${this.config.endpoint}/v1/logs/${logId}?${params.toString()}`,
442
+ { method: "GET" }
443
+ );
444
+ return await response.json();
445
+ }
446
+ async fetchWithRetry(url, options, attempt = 1) {
447
+ const response = await fetch(url, {
448
+ ...options,
449
+ headers: {
450
+ "Content-Type": "application/json",
451
+ Authorization: `Bearer ${this.config.apiKey}`,
452
+ ...options.headers
453
+ }
454
+ });
455
+ if (!response.ok) {
456
+ if (response.status >= 500 && attempt < this.config.maxRetries) {
457
+ const delay = RETRY_BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 100;
458
+ await new Promise((resolve) => setTimeout(resolve, delay));
459
+ return this.fetchWithRetry(url, options, attempt + 1);
460
+ }
461
+ let errorData = {
462
+ code: "REQUEST_ERROR",
463
+ message: `Request failed: ${response.status}`
464
+ };
465
+ try {
466
+ const data = await response.json();
467
+ if (data && data.code && data.message) {
468
+ errorData = data;
469
+ }
470
+ } catch {
471
+ }
472
+ throw new LogStackError(
473
+ errorData.code,
474
+ errorData.message,
475
+ response.status
476
+ );
477
+ }
478
+ return response;
479
+ }
480
+ async close() {
481
+ this.isClosing = true;
482
+ this.stopFlushTimer();
483
+ if (this.offlineRetryTimer) {
484
+ clearInterval(this.offlineRetryTimer);
485
+ this.offlineRetryTimer = null;
486
+ }
487
+ if (!this.config.disabled && this.buffer.length > 0) {
488
+ await this.flush();
489
+ }
490
+ if (this.isOnline && this.offlineQueue.length > 0) {
491
+ try {
492
+ await this.flushOfflineQueue();
493
+ } catch {
494
+ }
495
+ }
496
+ }
497
+ };
498
+ function createLogStack(config) {
499
+ return new LogStack(config);
500
+ }
501
+ // Annotate the CommonJS export names for ESM import in node:
502
+ 0 && (module.exports = {
503
+ LogStackError,
504
+ createLogStack
505
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,477 @@
1
+ // src/types.ts
2
+ var LogStackError = class extends Error {
3
+ constructor(code, message, status) {
4
+ super(message);
5
+ this.code = code;
6
+ this.status = status;
7
+ this.name = "LogStackError";
8
+ }
9
+ };
10
+
11
+ // src/index.ts
12
+ var DEFAULT_ENDPOINT = "https://api.logstack.tech";
13
+ var DEFAULT_BATCH_SIZE = 100;
14
+ var DEFAULT_FLUSH_INTERVAL = 5e3;
15
+ var DEFAULT_MAX_RETRIES = 3;
16
+ var DEFAULT_MAX_OFFLINE_QUEUE = 1e3;
17
+ var RETRY_BASE_DELAY = 1e3;
18
+ var OFFLINE_STORAGE_KEY = "logstack_offline_queue";
19
+ var OFFLINE_STORAGE_TTL = 24 * 60 * 60 * 1e3;
20
+ var LEVEL_COLORS = {
21
+ debug: "\x1B[35m",
22
+ // Magenta
23
+ info: "\x1B[36m",
24
+ // Cyan
25
+ warn: "\x1B[33m",
26
+ // Yellow
27
+ error: "\x1B[31m",
28
+ // Red
29
+ critical: "\x1B[41m\x1B[37m",
30
+ // White on Red background
31
+ fatal: "\x1B[45m\x1B[37m"
32
+ // White on Magenta background
33
+ };
34
+ var RESET_COLOR = "\x1B[0m";
35
+ var BROWSER_STYLES = {
36
+ debug: "color: #a371f7; font-weight: bold",
37
+ info: "color: #58a6ff; font-weight: bold",
38
+ warn: "color: #d29922; font-weight: bold",
39
+ error: "color: #f85149; font-weight: bold",
40
+ critical: "background: #da3633; color: white; font-weight: bold; padding: 2px 6px; border-radius: 2px",
41
+ fatal: "background: #8957e5; color: white; font-weight: bold; padding: 2px 6px; border-radius: 2px"
42
+ };
43
+ function isBrowserEnvironment() {
44
+ return typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
45
+ }
46
+ function getBrowserWindow() {
47
+ if (!isBrowserEnvironment()) {
48
+ return void 0;
49
+ }
50
+ const win = globalThis.window;
51
+ if (typeof win !== "object" || win === null) {
52
+ return void 0;
53
+ }
54
+ return win;
55
+ }
56
+ function getBrowserStorage() {
57
+ if (!isBrowserEnvironment()) {
58
+ return void 0;
59
+ }
60
+ const storage = globalThis.localStorage;
61
+ if (typeof storage !== "object" || storage === null) {
62
+ return void 0;
63
+ }
64
+ return storage;
65
+ }
66
+ function getBrowserContext() {
67
+ const win = getBrowserWindow();
68
+ if (!win) {
69
+ return {};
70
+ }
71
+ return {
72
+ url: win.location?.href,
73
+ route: win.location?.pathname,
74
+ userAgent: win.navigator?.userAgent
75
+ };
76
+ }
77
+ var LogStack = class {
78
+ constructor(config) {
79
+ this.buffer = [];
80
+ this.flushTimer = null;
81
+ this.isFlushing = false;
82
+ this.isClosing = false;
83
+ this.globalContext = {};
84
+ this.isOnline = true;
85
+ this.offlineQueue = [];
86
+ this.offlineRetryTimer = null;
87
+ this.isBrowser = isBrowserEnvironment();
88
+ const detectedEnvironment = this.detectEnvironment();
89
+ this.config = {
90
+ apiKey: config.apiKey,
91
+ endpoint: config.endpoint || DEFAULT_ENDPOINT,
92
+ batchSize: config.batchSize || DEFAULT_BATCH_SIZE,
93
+ flushInterval: config.flushInterval || DEFAULT_FLUSH_INTERVAL,
94
+ maxRetries: config.maxRetries || DEFAULT_MAX_RETRIES,
95
+ environment: config.environment || detectedEnvironment,
96
+ consoleInProduction: config.consoleInProduction || false,
97
+ silent: config.silent || false,
98
+ disabled: config.disabled || false,
99
+ maxOfflineQueueSize: config.maxOfflineQueueSize ?? DEFAULT_MAX_OFFLINE_QUEUE,
100
+ captureContext: config.captureContext !== false,
101
+ onError: config.onError,
102
+ projectId: config.projectId
103
+ };
104
+ if (!this.config.disabled) {
105
+ this.startFlushTimer();
106
+ }
107
+ if (this.config.captureContext && this.isBrowser) {
108
+ this.captureDefaultContext();
109
+ }
110
+ if (this.isBrowser && !this.config.disabled) {
111
+ this.setupOfflineDetection();
112
+ this.restoreOfflineQueue();
113
+ }
114
+ }
115
+ detectEnvironment() {
116
+ if (typeof process !== "undefined" && process.env) {
117
+ const nodeEnv = process.env.NODE_ENV;
118
+ if (nodeEnv === "development" || nodeEnv === "dev") return "development";
119
+ if (nodeEnv === "test") return "test";
120
+ if (nodeEnv === "staging") return "staging";
121
+ }
122
+ return "production";
123
+ }
124
+ setupOfflineDetection() {
125
+ const win = getBrowserWindow();
126
+ if (!win) return;
127
+ this.isOnline = win.navigator?.onLine ?? true;
128
+ win.addEventListener?.("online", () => {
129
+ this.isOnline = true;
130
+ this.flushOfflineQueue().catch(() => {
131
+ });
132
+ });
133
+ win.addEventListener?.("offline", () => {
134
+ this.isOnline = false;
135
+ });
136
+ }
137
+ async flushOfflineQueue() {
138
+ if (this.offlineQueue.length === 0) {
139
+ return;
140
+ }
141
+ const logsToSend = [...this.offlineQueue];
142
+ this.offlineQueue = [];
143
+ this.saveOfflineQueue();
144
+ try {
145
+ await this.sendLogs(logsToSend);
146
+ } catch (error) {
147
+ this.offlineQueue = [...logsToSend, ...this.offlineQueue];
148
+ this.saveOfflineQueue();
149
+ throw error;
150
+ }
151
+ }
152
+ saveOfflineQueue() {
153
+ if (!this.isBrowser) return;
154
+ const storage = getBrowserStorage();
155
+ if (!storage) return;
156
+ try {
157
+ const dataToStore = this.offlineQueue.map((log) => ({
158
+ ...log,
159
+ timestamp: log.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
160
+ savedAt: (/* @__PURE__ */ new Date()).getTime()
161
+ }));
162
+ storage.setItem(OFFLINE_STORAGE_KEY, JSON.stringify(dataToStore));
163
+ } catch (error) {
164
+ if (this.config.onError && error instanceof Error) {
165
+ this.config.onError(error, []);
166
+ }
167
+ }
168
+ }
169
+ restoreOfflineQueue() {
170
+ if (!this.isBrowser) return;
171
+ const storage = getBrowserStorage();
172
+ if (!storage) return;
173
+ try {
174
+ const stored = storage.getItem(OFFLINE_STORAGE_KEY);
175
+ if (stored) {
176
+ const logs = JSON.parse(stored);
177
+ const now = (/* @__PURE__ */ new Date()).getTime();
178
+ this.offlineQueue = logs.filter((log) => now - log.savedAt < OFFLINE_STORAGE_TTL).map(({ savedAt, ...log }) => log);
179
+ this.saveOfflineQueue();
180
+ }
181
+ } catch (error) {
182
+ storage.removeItem(OFFLINE_STORAGE_KEY);
183
+ }
184
+ }
185
+ isProductionMode() {
186
+ return this.config.environment === "production" || this.config.environment === "staging";
187
+ }
188
+ /**
189
+ * Whether this log should be written to the console. Always on in
190
+ * development/test; in production/staging only when consoleInProduction is set.
191
+ * `silent` disables console output entirely.
192
+ */
193
+ shouldLogToConsole() {
194
+ if (this.config.silent) {
195
+ return false;
196
+ }
197
+ if (this.isProductionMode()) {
198
+ return this.config.consoleInProduction;
199
+ }
200
+ return true;
201
+ }
202
+ /**
203
+ * Append logs to the offline queue, dropping the oldest entries past the
204
+ * configured cap to avoid exceeding localStorage quota.
205
+ */
206
+ enqueueOffline(logs) {
207
+ this.offlineQueue.push(...logs);
208
+ const max = this.config.maxOfflineQueueSize;
209
+ if (this.offlineQueue.length > max) {
210
+ this.offlineQueue = this.offlineQueue.slice(-max);
211
+ }
212
+ this.saveOfflineQueue();
213
+ }
214
+ captureDefaultContext() {
215
+ this.globalContext = getBrowserContext();
216
+ }
217
+ formatTimestamp(timestamp) {
218
+ const date = new Date(timestamp);
219
+ return date.toISOString().replace("T", " ").slice(0, 23);
220
+ }
221
+ logToConsole(entry) {
222
+ const timestamp = this.formatTimestamp(
223
+ entry.timestamp || (/* @__PURE__ */ new Date()).toISOString()
224
+ );
225
+ const level = entry.level.toUpperCase().padEnd(8);
226
+ if (this.isBrowser) {
227
+ const style = BROWSER_STYLES[entry.level];
228
+ const contextStr = entry.context?.url ? ` [${entry.context.url}]` : "";
229
+ console.log(
230
+ `%c${level}%c ${timestamp}${contextStr} - ${entry.message}`,
231
+ style,
232
+ "color: inherit",
233
+ entry.metadata || ""
234
+ );
235
+ } else {
236
+ const color = LEVEL_COLORS[entry.level];
237
+ const contextStr = entry.context?.url ? ` [${entry.context.url}]` : entry.context?.route ? ` [${entry.context.route}]` : "";
238
+ const metaStr = entry.metadata ? ` ${JSON.stringify(entry.metadata)}` : "";
239
+ console.log(
240
+ `${color}${level}${RESET_COLOR} ${timestamp}${contextStr} - ${entry.message}${metaStr}`
241
+ );
242
+ }
243
+ }
244
+ startFlushTimer() {
245
+ this.flushTimer = setInterval(() => {
246
+ if (this.buffer.length > 0 && !this.isFlushing) {
247
+ this.flush().catch(() => {
248
+ });
249
+ }
250
+ }, this.config.flushInterval);
251
+ }
252
+ stopFlushTimer() {
253
+ if (this.flushTimer) {
254
+ clearInterval(this.flushTimer);
255
+ this.flushTimer = null;
256
+ }
257
+ }
258
+ setContext(context) {
259
+ this.globalContext = { ...this.globalContext, ...context };
260
+ }
261
+ clearContext() {
262
+ this.globalContext = {};
263
+ if (this.config.captureContext && this.isBrowser) {
264
+ this.captureDefaultContext();
265
+ }
266
+ }
267
+ debug(message, metadata) {
268
+ this.log({ level: "debug", message, metadata });
269
+ }
270
+ info(message, metadata) {
271
+ this.log({ level: "info", message, metadata });
272
+ }
273
+ warn(message, metadata) {
274
+ this.log({ level: "warn", message, metadata });
275
+ }
276
+ error(message, metadata) {
277
+ this.log({ level: "error", message, metadata });
278
+ }
279
+ critical(message, metadata) {
280
+ this.log({ level: "critical", message, metadata });
281
+ }
282
+ fatal(message, metadata) {
283
+ this.log({ level: "fatal", message, metadata });
284
+ }
285
+ log(entry) {
286
+ if (this.isClosing) {
287
+ console.warn("Logstack: Cannot log after close() has been called");
288
+ return;
289
+ }
290
+ const timestamp = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
291
+ const context = {
292
+ ...this.globalContext,
293
+ ...entry.context
294
+ };
295
+ const logEntry = {
296
+ ...entry,
297
+ timestamp,
298
+ context: Object.keys(context).length > 0 ? context : void 0
299
+ };
300
+ if (this.shouldLogToConsole()) {
301
+ this.logToConsole(logEntry);
302
+ }
303
+ if (this.config.disabled) {
304
+ return;
305
+ }
306
+ if (!this.isOnline) {
307
+ this.enqueueOffline([logEntry]);
308
+ return;
309
+ }
310
+ this.buffer.push(logEntry);
311
+ if (this.buffer.length >= this.config.batchSize && !this.isFlushing) {
312
+ this.flush().catch(() => {
313
+ });
314
+ }
315
+ }
316
+ async flush() {
317
+ if (this.config.disabled) {
318
+ return;
319
+ }
320
+ if (this.buffer.length === 0 || this.isFlushing) {
321
+ return;
322
+ }
323
+ this.isFlushing = true;
324
+ const logsToSend = [...this.buffer];
325
+ this.buffer = [];
326
+ try {
327
+ await this.sendLogs(logsToSend);
328
+ } catch (error) {
329
+ this.buffer = [...logsToSend, ...this.buffer];
330
+ if (this.config.onError && error instanceof Error) {
331
+ this.config.onError(error, logsToSend);
332
+ }
333
+ throw error;
334
+ } finally {
335
+ this.isFlushing = false;
336
+ }
337
+ }
338
+ async sendLogs(logs, attempt = 1) {
339
+ try {
340
+ const response = await fetch(`${this.config.endpoint}/v1/logs`, {
341
+ method: "POST",
342
+ headers: {
343
+ "Content-Type": "application/json",
344
+ Authorization: `Bearer ${this.config.apiKey}`
345
+ },
346
+ body: JSON.stringify({ logs })
347
+ });
348
+ if (!response.ok) {
349
+ if (response.status >= 500 && attempt < this.config.maxRetries) {
350
+ const delay = RETRY_BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 100;
351
+ await new Promise((resolve) => setTimeout(resolve, delay));
352
+ return this.sendLogs(logs, attempt + 1);
353
+ }
354
+ let errorData = {
355
+ code: "SEND_ERROR",
356
+ message: `Failed to send logs: ${response.status}`
357
+ };
358
+ try {
359
+ const data = await response.json();
360
+ if (data && data.code && data.message) {
361
+ errorData = data;
362
+ }
363
+ } catch {
364
+ }
365
+ throw new LogStackError(
366
+ errorData.code,
367
+ errorData.message,
368
+ response.status
369
+ );
370
+ }
371
+ } catch (error) {
372
+ if (this.isOnline && error instanceof Error) {
373
+ this.enqueueOffline(logs);
374
+ this.isOnline = false;
375
+ throw error;
376
+ }
377
+ throw error;
378
+ }
379
+ }
380
+ async getLogs(options = {}) {
381
+ if (!this.config.projectId) {
382
+ throw new LogStackError(
383
+ "MISSING_PROJECT_ID",
384
+ "projectId is required in config for query operations",
385
+ 400
386
+ );
387
+ }
388
+ const params = new URLSearchParams();
389
+ params.set("projectId", this.config.projectId);
390
+ if (options.level) params.set("level", options.level);
391
+ if (options.search) params.set("search", options.search);
392
+ if (options.startTime) params.set("startTime", options.startTime);
393
+ if (options.endTime) params.set("endTime", options.endTime);
394
+ if (options.offset !== void 0)
395
+ params.set("offset", String(options.offset));
396
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
397
+ const response = await this.fetchWithRetry(
398
+ `${this.config.endpoint}/v1/logs?${params.toString()}`,
399
+ { method: "GET" }
400
+ );
401
+ return await response.json();
402
+ }
403
+ async getLogById(logId) {
404
+ if (!this.config.projectId) {
405
+ throw new LogStackError(
406
+ "MISSING_PROJECT_ID",
407
+ "projectId is required in config for query operations",
408
+ 400
409
+ );
410
+ }
411
+ const params = new URLSearchParams();
412
+ params.set("projectId", this.config.projectId);
413
+ const response = await this.fetchWithRetry(
414
+ `${this.config.endpoint}/v1/logs/${logId}?${params.toString()}`,
415
+ { method: "GET" }
416
+ );
417
+ return await response.json();
418
+ }
419
+ async fetchWithRetry(url, options, attempt = 1) {
420
+ const response = await fetch(url, {
421
+ ...options,
422
+ headers: {
423
+ "Content-Type": "application/json",
424
+ Authorization: `Bearer ${this.config.apiKey}`,
425
+ ...options.headers
426
+ }
427
+ });
428
+ if (!response.ok) {
429
+ if (response.status >= 500 && attempt < this.config.maxRetries) {
430
+ const delay = RETRY_BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 100;
431
+ await new Promise((resolve) => setTimeout(resolve, delay));
432
+ return this.fetchWithRetry(url, options, attempt + 1);
433
+ }
434
+ let errorData = {
435
+ code: "REQUEST_ERROR",
436
+ message: `Request failed: ${response.status}`
437
+ };
438
+ try {
439
+ const data = await response.json();
440
+ if (data && data.code && data.message) {
441
+ errorData = data;
442
+ }
443
+ } catch {
444
+ }
445
+ throw new LogStackError(
446
+ errorData.code,
447
+ errorData.message,
448
+ response.status
449
+ );
450
+ }
451
+ return response;
452
+ }
453
+ async close() {
454
+ this.isClosing = true;
455
+ this.stopFlushTimer();
456
+ if (this.offlineRetryTimer) {
457
+ clearInterval(this.offlineRetryTimer);
458
+ this.offlineRetryTimer = null;
459
+ }
460
+ if (!this.config.disabled && this.buffer.length > 0) {
461
+ await this.flush();
462
+ }
463
+ if (this.isOnline && this.offlineQueue.length > 0) {
464
+ try {
465
+ await this.flushOfflineQueue();
466
+ } catch {
467
+ }
468
+ }
469
+ }
470
+ };
471
+ function createLogStack(config) {
472
+ return new LogStack(config);
473
+ }
474
+ export {
475
+ LogStackError,
476
+ createLogStack
477
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "logstack-js",
3
+ "version": "1.0.0",
4
+ "description": "Logstack JavaScript/TypeScript SDK for log ingestion and querying",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts",
22
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
23
+ "lint": "eslint src --ext .ts",
24
+ "test": "vitest",
25
+ "prepublishOnly": "pnpm build"
26
+ },
27
+ "keywords": [
28
+ "logstack",
29
+ "logging",
30
+ "logs",
31
+ "monitoring",
32
+ "observability"
33
+ ],
34
+ "author": "Mosesedem",
35
+ "license": "MIT",
36
+ "homepage": "https://github.com/Mosesedem/logstack/tree/main/packages/logstack-js#readme",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/Mosesedem/logstack.git",
40
+ "directory": "packages/logstack-js"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/Mosesedem/logstack/issues"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^20.10.0",
50
+ "eslint": "^8.55.0",
51
+ "tsup": "^8.0.1",
52
+ "typescript": "^5.3.3",
53
+ "vitest": "^1.1.0"
54
+ },
55
+ "engines": {
56
+ "node": ">=16"
57
+ }
58
+ }