@squasher-ai/browser-logger 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) 2026 Squasher
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,12 @@
1
+ # @squasher-ai/browser-logger
2
+
3
+ Squasher browser console transport
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @squasher-ai/browser-logger
9
+ ```
10
+
11
+ Docs: https://docs.squasher.ai/sdks/browser-logger
12
+ Homepage: https://squasher.ai
@@ -0,0 +1,13 @@
1
+ import type { BrowserLoggerConfig } from "./types";
2
+ export declare class BrowserLogger {
3
+ private readonly config;
4
+ private readonly transport;
5
+ private readonly originalConsoleError;
6
+ private installed;
7
+ constructor(config: BrowserLoggerConfig);
8
+ flush(): Promise<void>;
9
+ close(): Promise<void>;
10
+ private installConsoleErrorCapture;
11
+ private restoreConsoleErrorCapture;
12
+ }
13
+ //# sourceMappingURL=browser-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-logger.d.ts","sourceRoot":"","sources":["../src/browser-logger.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAWnD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAuB;IAC5D,OAAO,CAAC,SAAS,CAAS;gBAEd,MAAM,EAAE,mBAAmB;IAkBjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,OAAO,CAAC,0BAA0B;IAelC,OAAO,CAAC,0BAA0B;CAQnC"}
@@ -0,0 +1,56 @@
1
+ import { buildConsoleEvent } from "./serializer";
2
+ import { BrowserLoggerTransport } from "./transport";
3
+ const DEFAULT_ENDPOINT = "https://ingest.squasher.ai";
4
+ export class BrowserLogger {
5
+ config;
6
+ transport;
7
+ originalConsoleError;
8
+ installed = false;
9
+ constructor(config) {
10
+ if (!config.apiKey) {
11
+ throw new Error("Squasher Browser Logger: apiKey is required");
12
+ }
13
+ if (!config.projectId) {
14
+ throw new Error("Squasher Browser Logger: projectId is required");
15
+ }
16
+ this.config = resolveConfig(config);
17
+ this.transport = new BrowserLoggerTransport(this.config);
18
+ this.originalConsoleError = console.error;
19
+ if (this.config.captureConsoleErrors) {
20
+ this.installConsoleErrorCapture();
21
+ }
22
+ }
23
+ async flush() {
24
+ await this.transport.flush();
25
+ }
26
+ async close() {
27
+ this.restoreConsoleErrorCapture();
28
+ await this.transport.close();
29
+ }
30
+ installConsoleErrorCapture() {
31
+ if (this.installed) {
32
+ return;
33
+ }
34
+ const originalConsoleError = this.originalConsoleError;
35
+ console.error = (...args) => {
36
+ Reflect.apply(originalConsoleError, console, args);
37
+ this.transport.enqueue(buildConsoleEvent(args, "error"));
38
+ };
39
+ this.installed = true;
40
+ }
41
+ restoreConsoleErrorCapture() {
42
+ if (!this.installed) {
43
+ return;
44
+ }
45
+ console.error = this.originalConsoleError;
46
+ this.installed = false;
47
+ }
48
+ }
49
+ function resolveConfig(config) {
50
+ return {
51
+ apiKey: config.apiKey,
52
+ projectId: config.projectId,
53
+ endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
54
+ captureConsoleErrors: config.captureConsoleErrors ?? true,
55
+ };
56
+ }
@@ -0,0 +1,420 @@
1
+ (() => {
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
7
+ var __toCommonJS = (from) => {
8
+ var entry = __moduleCache.get(from), desc;
9
+ if (entry)
10
+ return entry;
11
+ entry = __defProp({}, "__esModule", { value: true });
12
+ if (from && typeof from === "object" || typeof from === "function")
13
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ }));
17
+ __moduleCache.set(from, entry);
18
+ return entry;
19
+ };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true,
25
+ configurable: true,
26
+ set: (newValue) => all[name] = () => newValue
27
+ });
28
+ };
29
+
30
+ // src/global.ts
31
+ var exports_global = {};
32
+ __export(exports_global, {
33
+ initBrowserLogger: () => initBrowserLogger,
34
+ getBrowserLogger: () => getBrowserLogger,
35
+ flushBrowserLogger: () => flushBrowserLogger,
36
+ default: () => global_default,
37
+ closeBrowserLogger: () => closeBrowserLogger,
38
+ BrowserLogger: () => BrowserLogger
39
+ });
40
+
41
+ // src/serializer.ts
42
+ var SDK_NAME = "@squasher-ai/browser-logger";
43
+ var SDK_VERSION = "0.1.0";
44
+ function buildConsoleEvent(args, level) {
45
+ const error = findError(args);
46
+ const pageUrl = getPageUrl();
47
+ const userAgent = getUserAgent();
48
+ const extra = {
49
+ console_arguments: Array.from(args, (arg) => toJsonValue(arg)),
50
+ console_method: level === "warning" ? "warn" : "error"
51
+ };
52
+ if (pageUrl) {
53
+ extra.page_url = pageUrl;
54
+ }
55
+ if (userAgent) {
56
+ extra.user_agent = userAgent;
57
+ }
58
+ return {
59
+ message: buildMessage(args, error),
60
+ type: error?.name,
61
+ stack: error?.stack,
62
+ level,
63
+ request: pageUrl ? { url: pageUrl } : undefined,
64
+ sdk: {
65
+ name: SDK_NAME,
66
+ version: SDK_VERSION
67
+ },
68
+ timestamp: new Date().toISOString(),
69
+ extra
70
+ };
71
+ }
72
+ function buildMessage(args, error) {
73
+ if (args.length === 0) {
74
+ return "console.error";
75
+ }
76
+ const parts = Array.from(args, (arg) => formatMessagePart(arg)).filter((part) => part.length > 0);
77
+ if (parts.length > 0) {
78
+ return parts.join(" ");
79
+ }
80
+ if (error?.message) {
81
+ return error.message;
82
+ }
83
+ return "console.error";
84
+ }
85
+ function formatMessagePart(value) {
86
+ if (value instanceof Error) {
87
+ return value.message || value.name || "Error";
88
+ }
89
+ if (typeof value === "string") {
90
+ return value;
91
+ }
92
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
93
+ return String(value);
94
+ }
95
+ if (value === null) {
96
+ return "null";
97
+ }
98
+ if (typeof value === "undefined") {
99
+ return "undefined";
100
+ }
101
+ return stringifyJsonValue(toJsonValue(value));
102
+ }
103
+ function findError(args) {
104
+ return Array.from(args).find((arg) => arg instanceof Error);
105
+ }
106
+ function getPageUrl() {
107
+ if (typeof location === "undefined") {
108
+ return;
109
+ }
110
+ return location.href;
111
+ }
112
+ function getUserAgent() {
113
+ if (typeof navigator === "undefined") {
114
+ return;
115
+ }
116
+ return navigator.userAgent;
117
+ }
118
+ function stringifyJsonValue(value) {
119
+ if (typeof value === "string") {
120
+ return value;
121
+ }
122
+ return JSON.stringify(value);
123
+ }
124
+ function toJsonValue(value) {
125
+ if (value === null) {
126
+ return null;
127
+ }
128
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
129
+ return value;
130
+ }
131
+ if (typeof value === "bigint" || typeof value === "symbol") {
132
+ return String(value);
133
+ }
134
+ if (typeof value === "undefined") {
135
+ return "undefined";
136
+ }
137
+ if (typeof value === "function") {
138
+ return `[Function ${value.name || "anonymous"}]`;
139
+ }
140
+ if (value instanceof Error) {
141
+ const serializedError = {
142
+ name: value.name,
143
+ message: value.message
144
+ };
145
+ if (value.stack) {
146
+ serializedError.stack = value.stack;
147
+ }
148
+ return serializedError;
149
+ }
150
+ if (Array.isArray(value)) {
151
+ return value.map((item) => toJsonValue(item));
152
+ }
153
+ return serializeObject(value);
154
+ }
155
+ function serializeObject(value) {
156
+ const seen = new WeakSet;
157
+ const json = JSON.stringify(value, (_key, nestedValue) => {
158
+ if (typeof nestedValue === "bigint" || typeof nestedValue === "symbol") {
159
+ return String(nestedValue);
160
+ }
161
+ if (typeof nestedValue === "function") {
162
+ return `[Function ${nestedValue.name || "anonymous"}]`;
163
+ }
164
+ if (nestedValue instanceof Error) {
165
+ const serializedError = {
166
+ name: nestedValue.name,
167
+ message: nestedValue.message
168
+ };
169
+ if (nestedValue.stack) {
170
+ serializedError.stack = nestedValue.stack;
171
+ }
172
+ return serializedError;
173
+ }
174
+ if (typeof nestedValue === "object" && nestedValue !== null) {
175
+ if (seen.has(nestedValue)) {
176
+ return "[Circular]";
177
+ }
178
+ seen.add(nestedValue);
179
+ }
180
+ return nestedValue;
181
+ });
182
+ if (!json) {
183
+ return String(value);
184
+ }
185
+ return JSON.parse(json);
186
+ }
187
+
188
+ // src/transport.ts
189
+ var DEFAULT_ENDPOINT = "https://ingest.squasher.ai";
190
+ var DEFAULT_BATCH_SIZE = 10;
191
+ var DEFAULT_FLUSH_INTERVAL_MS = 5000;
192
+
193
+ class BrowserLoggerTransport {
194
+ apiKey;
195
+ projectId;
196
+ endpoint;
197
+ queue = [];
198
+ flushTimer = null;
199
+ closed = false;
200
+ handlePageHide = () => {
201
+ this.flushOnPageHide();
202
+ };
203
+ handleVisibilityChange = () => {
204
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
205
+ this.flushOnPageHide();
206
+ }
207
+ };
208
+ constructor(config) {
209
+ this.apiKey = config.apiKey;
210
+ this.projectId = config.projectId;
211
+ this.endpoint = normalizeEndpoint(config.endpoint ?? DEFAULT_ENDPOINT);
212
+ this.flushTimer = setInterval(() => {
213
+ this.flush();
214
+ }, DEFAULT_FLUSH_INTERVAL_MS);
215
+ this.installListeners();
216
+ }
217
+ enqueue(event) {
218
+ if (this.closed) {
219
+ return;
220
+ }
221
+ this.queue.push(event);
222
+ if (this.queue.length >= DEFAULT_BATCH_SIZE) {
223
+ this.flush();
224
+ }
225
+ }
226
+ async flush() {
227
+ if (this.closed || this.queue.length === 0) {
228
+ return;
229
+ }
230
+ const events = this.queue.splice(0);
231
+ const body = stringifyPayload(events);
232
+ const success = await this.sendFetch(body);
233
+ if (!success) {
234
+ this.queue.unshift(...events);
235
+ }
236
+ }
237
+ async close() {
238
+ if (this.closed) {
239
+ return;
240
+ }
241
+ this.removeListeners();
242
+ if (this.flushTimer) {
243
+ clearInterval(this.flushTimer);
244
+ this.flushTimer = null;
245
+ }
246
+ await this.flush();
247
+ this.closed = true;
248
+ }
249
+ flushOnPageHide() {
250
+ if (this.closed || this.queue.length === 0) {
251
+ return;
252
+ }
253
+ const events = this.queue.splice(0);
254
+ const body = stringifyPayload(events);
255
+ if (this.trySendBeacon(body)) {
256
+ return;
257
+ }
258
+ this.sendFetch(body).then((success) => {
259
+ if (!success) {
260
+ this.queue.unshift(...events);
261
+ }
262
+ });
263
+ }
264
+ installListeners() {
265
+ if (typeof document !== "undefined") {
266
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
267
+ }
268
+ if (typeof window !== "undefined") {
269
+ window.addEventListener("pagehide", this.handlePageHide);
270
+ window.addEventListener("beforeunload", this.handlePageHide);
271
+ }
272
+ }
273
+ removeListeners() {
274
+ if (typeof document !== "undefined") {
275
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
276
+ }
277
+ if (typeof window !== "undefined") {
278
+ window.removeEventListener("pagehide", this.handlePageHide);
279
+ window.removeEventListener("beforeunload", this.handlePageHide);
280
+ }
281
+ }
282
+ trySendBeacon(body) {
283
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") {
284
+ return false;
285
+ }
286
+ try {
287
+ const blob = new Blob([body], { type: "application/json" });
288
+ return navigator.sendBeacon(this.getBeaconUrl(), blob);
289
+ } catch {
290
+ return false;
291
+ }
292
+ }
293
+ async sendFetch(body) {
294
+ try {
295
+ const response = await fetch(this.getIngestUrl(), {
296
+ method: "POST",
297
+ headers: {
298
+ "Content-Type": "application/json",
299
+ "x-squasher-key": this.apiKey
300
+ },
301
+ body,
302
+ keepalive: true
303
+ });
304
+ return response.ok;
305
+ } catch {
306
+ return false;
307
+ }
308
+ }
309
+ getIngestUrl() {
310
+ return `${this.endpoint}/v1/ingest/${this.projectId}`;
311
+ }
312
+ getBeaconUrl() {
313
+ return `${this.getIngestUrl()}?key=${encodeURIComponent(this.apiKey)}`;
314
+ }
315
+ }
316
+ function normalizeEndpoint(endpoint) {
317
+ return endpoint.replace(/\/+$/, "");
318
+ }
319
+ function stringifyPayload(events) {
320
+ const payload = { events };
321
+ return JSON.stringify(payload);
322
+ }
323
+
324
+ // src/browser-logger.ts
325
+ var DEFAULT_ENDPOINT2 = "https://ingest.squasher.ai";
326
+
327
+ class BrowserLogger {
328
+ config;
329
+ transport;
330
+ originalConsoleError;
331
+ installed = false;
332
+ constructor(config) {
333
+ if (!config.apiKey) {
334
+ throw new Error("Squasher Browser Logger: apiKey is required");
335
+ }
336
+ if (!config.projectId) {
337
+ throw new Error("Squasher Browser Logger: projectId is required");
338
+ }
339
+ this.config = resolveConfig(config);
340
+ this.transport = new BrowserLoggerTransport(this.config);
341
+ this.originalConsoleError = console.error;
342
+ if (this.config.captureConsoleErrors) {
343
+ this.installConsoleErrorCapture();
344
+ }
345
+ }
346
+ async flush() {
347
+ await this.transport.flush();
348
+ }
349
+ async close() {
350
+ this.restoreConsoleErrorCapture();
351
+ await this.transport.close();
352
+ }
353
+ installConsoleErrorCapture() {
354
+ if (this.installed) {
355
+ return;
356
+ }
357
+ const originalConsoleError = this.originalConsoleError;
358
+ console.error = (...args) => {
359
+ Reflect.apply(originalConsoleError, console, args);
360
+ this.transport.enqueue(buildConsoleEvent(args, "error"));
361
+ };
362
+ this.installed = true;
363
+ }
364
+ restoreConsoleErrorCapture() {
365
+ if (!this.installed) {
366
+ return;
367
+ }
368
+ console.error = this.originalConsoleError;
369
+ this.installed = false;
370
+ }
371
+ }
372
+ function resolveConfig(config) {
373
+ return {
374
+ apiKey: config.apiKey,
375
+ projectId: config.projectId,
376
+ endpoint: config.endpoint ?? DEFAULT_ENDPOINT2,
377
+ captureConsoleErrors: config.captureConsoleErrors ?? true
378
+ };
379
+ }
380
+ // src/index.ts
381
+ var browserLogger = null;
382
+ function initBrowserLogger(config) {
383
+ if (browserLogger) {
384
+ throw new Error("Squasher Browser Logger already initialized. Call close() first.");
385
+ }
386
+ browserLogger = new BrowserLogger(config);
387
+ return browserLogger;
388
+ }
389
+ function getBrowserLogger() {
390
+ if (!browserLogger) {
391
+ throw new Error("Squasher Browser Logger is not initialized. Call init() first.");
392
+ }
393
+ return browserLogger;
394
+ }
395
+ async function flushBrowserLogger() {
396
+ await getBrowserLogger().flush();
397
+ }
398
+ async function closeBrowserLogger() {
399
+ if (!browserLogger) {
400
+ return;
401
+ }
402
+ const activeLogger = browserLogger;
403
+ browserLogger = null;
404
+ await activeLogger.close();
405
+ }
406
+
407
+ // src/global.ts
408
+ var browserLoggerGlobal = {
409
+ BrowserLogger,
410
+ close: closeBrowserLogger,
411
+ closeBrowserLogger,
412
+ flush: flushBrowserLogger,
413
+ flushBrowserLogger,
414
+ getBrowserLogger,
415
+ init: initBrowserLogger,
416
+ initBrowserLogger
417
+ };
418
+ globalThis.SquasherBrowserLogger = browserLoggerGlobal;
419
+ var global_default = browserLoggerGlobal;
420
+ })();
@@ -0,0 +1,14 @@
1
+ import { BrowserLogger, closeBrowserLogger, flushBrowserLogger, getBrowserLogger, initBrowserLogger } from "./index";
2
+ declare const browserLoggerGlobal: {
3
+ BrowserLogger: typeof BrowserLogger;
4
+ close: typeof closeBrowserLogger;
5
+ closeBrowserLogger: typeof closeBrowserLogger;
6
+ flush: typeof flushBrowserLogger;
7
+ flushBrowserLogger: typeof flushBrowserLogger;
8
+ getBrowserLogger: typeof getBrowserLogger;
9
+ init: typeof initBrowserLogger;
10
+ initBrowserLogger: typeof initBrowserLogger;
11
+ };
12
+ export default browserLoggerGlobal;
13
+ export { BrowserLogger, closeBrowserLogger, flushBrowserLogger, getBrowserLogger, initBrowserLogger, };
14
+ //# sourceMappingURL=global.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global.d.ts","sourceRoot":"","sources":["../src/global.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,QAAA,MAAM,mBAAmB;;;;;;;;;CASxB,CAAC;AAQF,eAAe,mBAAmB,CAAC;AACnC,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GAClB,CAAC"}
package/dist/global.js ADDED
@@ -0,0 +1,14 @@
1
+ import { BrowserLogger, closeBrowserLogger, flushBrowserLogger, getBrowserLogger, initBrowserLogger, } from "./index";
2
+ const browserLoggerGlobal = {
3
+ BrowserLogger,
4
+ close: closeBrowserLogger,
5
+ closeBrowserLogger,
6
+ flush: flushBrowserLogger,
7
+ flushBrowserLogger,
8
+ getBrowserLogger,
9
+ init: initBrowserLogger,
10
+ initBrowserLogger,
11
+ };
12
+ globalThis.SquasherBrowserLogger = browserLoggerGlobal;
13
+ export default browserLoggerGlobal;
14
+ export { BrowserLogger, closeBrowserLogger, flushBrowserLogger, getBrowserLogger, initBrowserLogger, };
@@ -0,0 +1,10 @@
1
+ export { BrowserLogger } from "./browser-logger";
2
+ export type { BrowserLoggerConfig, BrowserLoggerEvent, IngestBatchPayload, JsonArray, JsonObject, JsonValue, Level, } from "./types";
3
+ import { BrowserLogger } from "./browser-logger";
4
+ import type { BrowserLoggerConfig } from "./types";
5
+ export declare function initBrowserLogger(config: BrowserLoggerConfig): BrowserLogger;
6
+ export declare function getBrowserLogger(): BrowserLogger;
7
+ export declare function flushBrowserLogger(): Promise<void>;
8
+ export declare function closeBrowserLogger(): Promise<void>;
9
+ export { closeBrowserLogger as close, flushBrowserLogger as flush, initBrowserLogger as init };
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,SAAS,EACT,UAAU,EACV,SAAS,EACT,KAAK,GACN,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAInD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAO5E;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAMhD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAExD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQxD;AAED,OAAO,EAAE,kBAAkB,IAAI,KAAK,EAAE,kBAAkB,IAAI,KAAK,EAAE,iBAAiB,IAAI,IAAI,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ export { BrowserLogger } from "./browser-logger";
2
+ import { BrowserLogger } from "./browser-logger";
3
+ let browserLogger = null;
4
+ export function initBrowserLogger(config) {
5
+ if (browserLogger) {
6
+ throw new Error("Squasher Browser Logger already initialized. Call close() first.");
7
+ }
8
+ browserLogger = new BrowserLogger(config);
9
+ return browserLogger;
10
+ }
11
+ export function getBrowserLogger() {
12
+ if (!browserLogger) {
13
+ throw new Error("Squasher Browser Logger is not initialized. Call init() first.");
14
+ }
15
+ return browserLogger;
16
+ }
17
+ export async function flushBrowserLogger() {
18
+ await getBrowserLogger().flush();
19
+ }
20
+ export async function closeBrowserLogger() {
21
+ if (!browserLogger) {
22
+ return;
23
+ }
24
+ const activeLogger = browserLogger;
25
+ browserLogger = null;
26
+ await activeLogger.close();
27
+ }
28
+ export { closeBrowserLogger as close, flushBrowserLogger as flush, initBrowserLogger as init };
@@ -0,0 +1,5 @@
1
+ import type { BrowserLoggerEvent, Level } from "./types";
2
+ type ConsoleArgs = Parameters<typeof console.error>;
3
+ export declare function buildConsoleEvent(args: ConsoleArgs, level: Level): BrowserLoggerEvent;
4
+ export {};
5
+ //# sourceMappingURL=serializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAyB,KAAK,EAAE,MAAM,SAAS,CAAC;AAKhF,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AAGpD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,GAAG,kBAAkB,CA+BrF"}
@@ -0,0 +1,148 @@
1
+ const SDK_NAME = "@squasher-ai/browser-logger";
2
+ const SDK_VERSION = "0.1.0";
3
+ export function buildConsoleEvent(args, level) {
4
+ const error = findError(args);
5
+ const pageUrl = getPageUrl();
6
+ const userAgent = getUserAgent();
7
+ const extra = {
8
+ console_arguments: Array.from(args, (arg) => toJsonValue(arg)),
9
+ console_method: level === "warning" ? "warn" : "error",
10
+ };
11
+ if (pageUrl) {
12
+ extra.page_url = pageUrl;
13
+ }
14
+ if (userAgent) {
15
+ extra.user_agent = userAgent;
16
+ }
17
+ return {
18
+ message: buildMessage(args, error),
19
+ type: error?.name,
20
+ stack: error?.stack,
21
+ level,
22
+ request: pageUrl ? { url: pageUrl } : undefined,
23
+ sdk: {
24
+ name: SDK_NAME,
25
+ version: SDK_VERSION,
26
+ },
27
+ timestamp: new Date().toISOString(),
28
+ extra,
29
+ };
30
+ }
31
+ function buildMessage(args, error) {
32
+ if (args.length === 0) {
33
+ return "console.error";
34
+ }
35
+ const parts = Array.from(args, (arg) => formatMessagePart(arg)).filter((part) => part.length > 0);
36
+ if (parts.length > 0) {
37
+ return parts.join(" ");
38
+ }
39
+ if (error?.message) {
40
+ return error.message;
41
+ }
42
+ return "console.error";
43
+ }
44
+ function formatMessagePart(value) {
45
+ if (value instanceof Error) {
46
+ return value.message || value.name || "Error";
47
+ }
48
+ if (typeof value === "string") {
49
+ return value;
50
+ }
51
+ if (typeof value === "number" ||
52
+ typeof value === "boolean" ||
53
+ typeof value === "bigint" ||
54
+ typeof value === "symbol") {
55
+ return String(value);
56
+ }
57
+ if (value === null) {
58
+ return "null";
59
+ }
60
+ if (typeof value === "undefined") {
61
+ return "undefined";
62
+ }
63
+ return stringifyJsonValue(toJsonValue(value));
64
+ }
65
+ function findError(args) {
66
+ return Array.from(args).find((arg) => arg instanceof Error);
67
+ }
68
+ function getPageUrl() {
69
+ if (typeof location === "undefined") {
70
+ return undefined;
71
+ }
72
+ return location.href;
73
+ }
74
+ function getUserAgent() {
75
+ if (typeof navigator === "undefined") {
76
+ return undefined;
77
+ }
78
+ return navigator.userAgent;
79
+ }
80
+ function stringifyJsonValue(value) {
81
+ if (typeof value === "string") {
82
+ return value;
83
+ }
84
+ return JSON.stringify(value);
85
+ }
86
+ function toJsonValue(value) {
87
+ if (value === null) {
88
+ return null;
89
+ }
90
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
91
+ return value;
92
+ }
93
+ if (typeof value === "bigint" || typeof value === "symbol") {
94
+ return String(value);
95
+ }
96
+ if (typeof value === "undefined") {
97
+ return "undefined";
98
+ }
99
+ if (typeof value === "function") {
100
+ return `[Function ${value.name || "anonymous"}]`;
101
+ }
102
+ if (value instanceof Error) {
103
+ const serializedError = {
104
+ name: value.name,
105
+ message: value.message,
106
+ };
107
+ if (value.stack) {
108
+ serializedError.stack = value.stack;
109
+ }
110
+ return serializedError;
111
+ }
112
+ if (Array.isArray(value)) {
113
+ return value.map((item) => toJsonValue(item));
114
+ }
115
+ return serializeObject(value);
116
+ }
117
+ function serializeObject(value) {
118
+ const seen = new WeakSet();
119
+ const json = JSON.stringify(value, (_key, nestedValue) => {
120
+ if (typeof nestedValue === "bigint" || typeof nestedValue === "symbol") {
121
+ return String(nestedValue);
122
+ }
123
+ if (typeof nestedValue === "function") {
124
+ return `[Function ${nestedValue.name || "anonymous"}]`;
125
+ }
126
+ if (nestedValue instanceof Error) {
127
+ const serializedError = {
128
+ name: nestedValue.name,
129
+ message: nestedValue.message,
130
+ };
131
+ if (nestedValue.stack) {
132
+ serializedError.stack = nestedValue.stack;
133
+ }
134
+ return serializedError;
135
+ }
136
+ if (typeof nestedValue === "object" && nestedValue !== null) {
137
+ if (seen.has(nestedValue)) {
138
+ return "[Circular]";
139
+ }
140
+ seen.add(nestedValue);
141
+ }
142
+ return nestedValue;
143
+ });
144
+ if (!json) {
145
+ return String(value);
146
+ }
147
+ return JSON.parse(json);
148
+ }
@@ -0,0 +1,28 @@
1
+ import type { BrowserLoggerEvent } from "./types";
2
+ export interface BrowserLoggerTransportConfig {
3
+ apiKey: string;
4
+ projectId: string;
5
+ endpoint?: string;
6
+ }
7
+ export declare class BrowserLoggerTransport {
8
+ private readonly apiKey;
9
+ private readonly projectId;
10
+ private readonly endpoint;
11
+ private readonly queue;
12
+ private flushTimer;
13
+ private closed;
14
+ private readonly handlePageHide;
15
+ private readonly handleVisibilityChange;
16
+ constructor(config: BrowserLoggerTransportConfig);
17
+ enqueue(event: BrowserLoggerEvent): void;
18
+ flush(): Promise<void>;
19
+ close(): Promise<void>;
20
+ private flushOnPageHide;
21
+ private installListeners;
22
+ private removeListeners;
23
+ private trySendBeacon;
24
+ private sendFetch;
25
+ private getIngestUrl;
26
+ private getBeaconUrl;
27
+ }
28
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAsB,MAAM,SAAS,CAAC;AAMtE,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4B;IAClD,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAE7B;IAEF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAIrC;gBAEU,MAAM,EAAE,4BAA4B;IAYhD,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAWlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IActB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB5B,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,aAAa;YAaP,SAAS;IAkBvB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,YAAY;CAGrB"}
@@ -0,0 +1,135 @@
1
+ const DEFAULT_ENDPOINT = "https://ingest.squasher.ai";
2
+ const DEFAULT_BATCH_SIZE = 10;
3
+ const DEFAULT_FLUSH_INTERVAL_MS = 5_000;
4
+ export class BrowserLoggerTransport {
5
+ apiKey;
6
+ projectId;
7
+ endpoint;
8
+ queue = [];
9
+ flushTimer = null;
10
+ closed = false;
11
+ handlePageHide = () => {
12
+ this.flushOnPageHide();
13
+ };
14
+ handleVisibilityChange = () => {
15
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
16
+ this.flushOnPageHide();
17
+ }
18
+ };
19
+ constructor(config) {
20
+ this.apiKey = config.apiKey;
21
+ this.projectId = config.projectId;
22
+ this.endpoint = normalizeEndpoint(config.endpoint ?? DEFAULT_ENDPOINT);
23
+ this.flushTimer = setInterval(() => {
24
+ void this.flush();
25
+ }, DEFAULT_FLUSH_INTERVAL_MS);
26
+ this.installListeners();
27
+ }
28
+ enqueue(event) {
29
+ if (this.closed) {
30
+ return;
31
+ }
32
+ this.queue.push(event);
33
+ if (this.queue.length >= DEFAULT_BATCH_SIZE) {
34
+ void this.flush();
35
+ }
36
+ }
37
+ async flush() {
38
+ if (this.closed || this.queue.length === 0) {
39
+ return;
40
+ }
41
+ const events = this.queue.splice(0);
42
+ const body = stringifyPayload(events);
43
+ const success = await this.sendFetch(body);
44
+ if (!success) {
45
+ this.queue.unshift(...events);
46
+ }
47
+ }
48
+ async close() {
49
+ if (this.closed) {
50
+ return;
51
+ }
52
+ this.removeListeners();
53
+ if (this.flushTimer) {
54
+ clearInterval(this.flushTimer);
55
+ this.flushTimer = null;
56
+ }
57
+ await this.flush();
58
+ this.closed = true;
59
+ }
60
+ flushOnPageHide() {
61
+ if (this.closed || this.queue.length === 0) {
62
+ return;
63
+ }
64
+ const events = this.queue.splice(0);
65
+ const body = stringifyPayload(events);
66
+ if (this.trySendBeacon(body)) {
67
+ return;
68
+ }
69
+ void this.sendFetch(body).then((success) => {
70
+ if (!success) {
71
+ this.queue.unshift(...events);
72
+ }
73
+ });
74
+ }
75
+ installListeners() {
76
+ if (typeof document !== "undefined") {
77
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
78
+ }
79
+ if (typeof window !== "undefined") {
80
+ window.addEventListener("pagehide", this.handlePageHide);
81
+ window.addEventListener("beforeunload", this.handlePageHide);
82
+ }
83
+ }
84
+ removeListeners() {
85
+ if (typeof document !== "undefined") {
86
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
87
+ }
88
+ if (typeof window !== "undefined") {
89
+ window.removeEventListener("pagehide", this.handlePageHide);
90
+ window.removeEventListener("beforeunload", this.handlePageHide);
91
+ }
92
+ }
93
+ trySendBeacon(body) {
94
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") {
95
+ return false;
96
+ }
97
+ try {
98
+ const blob = new Blob([body], { type: "application/json" });
99
+ return navigator.sendBeacon(this.getBeaconUrl(), blob);
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ async sendFetch(body) {
106
+ try {
107
+ const response = await fetch(this.getIngestUrl(), {
108
+ method: "POST",
109
+ headers: {
110
+ "Content-Type": "application/json",
111
+ "x-squasher-key": this.apiKey,
112
+ },
113
+ body,
114
+ keepalive: true,
115
+ });
116
+ return response.ok;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
122
+ getIngestUrl() {
123
+ return `${this.endpoint}/v1/ingest/${this.projectId}`;
124
+ }
125
+ getBeaconUrl() {
126
+ return `${this.getIngestUrl()}?key=${encodeURIComponent(this.apiKey)}`;
127
+ }
128
+ }
129
+ function normalizeEndpoint(endpoint) {
130
+ return endpoint.replace(/\/+$/, "");
131
+ }
132
+ function stringifyPayload(events) {
133
+ const payload = { events };
134
+ return JSON.stringify(payload);
135
+ }
@@ -0,0 +1,33 @@
1
+ export interface BrowserLoggerConfig {
2
+ apiKey: string;
3
+ projectId: string;
4
+ endpoint?: string;
5
+ captureConsoleErrors?: boolean;
6
+ }
7
+ export type Level = "error" | "warning";
8
+ export type JsonPrimitive = string | number | boolean | null;
9
+ export interface JsonArray extends Array<JsonValue> {
10
+ }
11
+ export interface JsonObject {
12
+ [key: string]: JsonValue | undefined;
13
+ }
14
+ export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
15
+ export interface BrowserLoggerEvent {
16
+ message: string;
17
+ type?: string;
18
+ stack?: string;
19
+ level: Level;
20
+ request?: {
21
+ url?: string;
22
+ };
23
+ sdk: {
24
+ name: string;
25
+ version: string;
26
+ };
27
+ timestamp: string;
28
+ extra?: JsonObject;
29
+ }
30
+ export interface IngestBatchPayload {
31
+ events: BrowserLoggerEvent[];
32
+ }
33
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,SAAS,CAAC;AAExC,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAE7D,MAAM,WAAW,SAAU,SAAQ,KAAK,CAAC,SAAS,CAAC;CAAG;AAEtD,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,SAAS,CAAC;AAE/D,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,CAAC,EAAE;QACR,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;IACF,GAAG,EAAE;QACH,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,kBAAkB,EAAE,CAAC;CAC9B"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@squasher-ai/browser-logger",
3
+ "version": "0.1.0",
4
+ "description": "Squasher browser console transport",
5
+ "homepage": "https://squasher.ai",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/squasher-ai/squasher.git",
10
+ "directory": "packages/browser-logger"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/squasher-ai/squasher/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "import": "./dist/index.js",
24
+ "types": "./dist/index.d.ts"
25
+ }
26
+ },
27
+ "keywords": [
28
+ "squasher",
29
+ "browser",
30
+ "logger",
31
+ "console",
32
+ "transport",
33
+ "error-monitoring"
34
+ ]
35
+ }