trashlytics 0.0.1

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/dist/index.cjs ADDED
@@ -0,0 +1,463 @@
1
+ let effect = require("effect");
2
+
3
+ //#region src/logger.ts
4
+ /**
5
+ * A logger that outputs to the console.
6
+ * This is the default logger.
7
+ */
8
+ const consoleLogger = {
9
+ debug: (message, ...args) => {
10
+ if (typeof console !== "undefined" && console.debug) console.debug(`[trashlytics] ${message}`, ...args);
11
+ },
12
+ info: (message, ...args) => {
13
+ if (typeof console !== "undefined" && console.info) console.info(`[trashlytics] ${message}`, ...args);
14
+ },
15
+ warn: (message, ...args) => {
16
+ if (typeof console !== "undefined" && console.warn) console.warn(`[trashlytics] ${message}`, ...args);
17
+ },
18
+ error: (message, ...args) => {
19
+ if (typeof console !== "undefined" && console.error) console.error(`[trashlytics] ${message}`, ...args);
20
+ }
21
+ };
22
+ /**
23
+ * A logger that does nothing.
24
+ * Use this to silence all library logging.
25
+ */
26
+ const noopLogger = {
27
+ debug: () => {},
28
+ info: () => {},
29
+ warn: () => {},
30
+ error: () => {}
31
+ };
32
+ /**
33
+ * Create a logger that only logs messages at or above the specified level.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { createMinLevelLogger } from "trashlytics"
38
+ *
39
+ * // Only log warnings and errors
40
+ * const logger = createMinLevelLogger("warn")
41
+ * ```
42
+ */
43
+ const createMinLevelLogger = (minLevel) => {
44
+ const levels = {
45
+ debug: 0,
46
+ info: 1,
47
+ warn: 2,
48
+ error: 3
49
+ };
50
+ const minOrdinal = levels[minLevel];
51
+ const shouldLog = (level) => levels[level] >= minOrdinal;
52
+ return {
53
+ debug: (message, ...args) => {
54
+ if (shouldLog("debug")) consoleLogger.debug(message, ...args);
55
+ },
56
+ info: (message, ...args) => {
57
+ if (shouldLog("info")) consoleLogger.info(message, ...args);
58
+ },
59
+ warn: (message, ...args) => {
60
+ if (shouldLog("warn")) consoleLogger.warn(message, ...args);
61
+ },
62
+ error: (message, ...args) => {
63
+ if (shouldLog("error")) consoleLogger.error(message, ...args);
64
+ }
65
+ };
66
+ };
67
+
68
+ //#endregion
69
+ //#region src/config.ts
70
+ /**
71
+ * Default configuration values.
72
+ */
73
+ const defaults = {
74
+ batchSize: 10,
75
+ flushIntervalMs: 5e3,
76
+ queueCapacity: 1e3,
77
+ queueStrategy: "dropping",
78
+ retryAttempts: 3,
79
+ retryDelayMs: 1e3,
80
+ shutdownTimeoutMs: 3e4
81
+ };
82
+ /**
83
+ * Resolves a partial config with defaults.
84
+ */
85
+ const resolveConfig = (config) => {
86
+ return {
87
+ batchSize: config.batchSize ?? defaults.batchSize,
88
+ flushIntervalMs: config.flushIntervalMs ?? defaults.flushIntervalMs,
89
+ queueCapacity: config.queueCapacity ?? defaults.queueCapacity,
90
+ queueStrategy: config.queueStrategy ?? defaults.queueStrategy,
91
+ retryAttempts: config.retryAttempts ?? defaults.retryAttempts,
92
+ retryDelayMs: config.retryDelayMs ?? defaults.retryDelayMs,
93
+ shutdownTimeoutMs: config.shutdownTimeoutMs ?? defaults.shutdownTimeoutMs,
94
+ transports: config.transports,
95
+ generateId: config.generateId,
96
+ metadata: config.metadata,
97
+ onError: config.onError,
98
+ logger: config.logger ?? consoleLogger
99
+ };
100
+ };
101
+
102
+ //#endregion
103
+ //#region src/event.ts
104
+ /**
105
+ * Creates a new event with the given name, payload, and options.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const event = createEvent("page_view", { page: "/home" }, {
110
+ * id: "abc123",
111
+ * metadata: { userId: "user_1" }
112
+ * })
113
+ * ```
114
+ */
115
+ const createEvent = (name, payload, options) => ({
116
+ id: options.id,
117
+ name,
118
+ timestamp: options.timestamp ?? Date.now(),
119
+ payload,
120
+ metadata: options.metadata ?? {}
121
+ });
122
+
123
+ //#endregion
124
+ //#region src/middleware.ts
125
+ /**
126
+ * Identity middleware - passes events through unchanged.
127
+ */
128
+ const identity = (event) => event;
129
+ /**
130
+ * Compose multiple middlewares into one.
131
+ * Middlewares are applied left to right.
132
+ * If any middleware returns null, the event is filtered out.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const middleware = compose(
137
+ * addMetadata({ appVersion: "1.0.0" }),
138
+ * filter((e) => e.name !== "internal"),
139
+ * )
140
+ * ```
141
+ */
142
+ const compose = (...middlewares) => {
143
+ if (middlewares.length === 0) return identity;
144
+ return (event) => {
145
+ let current = event;
146
+ for (const mw of middlewares) {
147
+ if (current === null) return null;
148
+ current = mw(current);
149
+ }
150
+ return current;
151
+ };
152
+ };
153
+ /**
154
+ * Filter events based on a predicate.
155
+ * Events that don't match the predicate are dropped.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * // Only track production events
160
+ * const prodOnly = filter(() => process.env.NODE_ENV === "production")
161
+ *
162
+ * // Skip internal events
163
+ * const skipInternal = filter((e) => !e.name.startsWith("_"))
164
+ * ```
165
+ */
166
+ const filter = (predicate) => {
167
+ return (event) => predicate(event) ? event : null;
168
+ };
169
+ /**
170
+ * Add static metadata to events.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * const addVersion = addMetadata({
175
+ * appVersion: "1.0.0",
176
+ * environment: "production",
177
+ * })
178
+ * ```
179
+ */
180
+ const addMetadata = (metadata) => {
181
+ return (event) => ({
182
+ ...event,
183
+ metadata: {
184
+ ...event.metadata,
185
+ ...metadata
186
+ }
187
+ });
188
+ };
189
+ /**
190
+ * Add metadata dynamically based on the event.
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * const addTimezone = addMetadataFrom(() => ({
195
+ * timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
196
+ * }))
197
+ * ```
198
+ */
199
+ const addMetadataFrom = (fn) => {
200
+ return (event) => ({
201
+ ...event,
202
+ metadata: {
203
+ ...event.metadata,
204
+ ...fn(event)
205
+ }
206
+ });
207
+ };
208
+ /**
209
+ * Transform the event name.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * // Prefix all event names
214
+ * const prefixed = mapName((name) => `app.${name}`)
215
+ * ```
216
+ */
217
+ const mapName = (fn) => {
218
+ return (event) => ({
219
+ ...event,
220
+ name: fn(event.name)
221
+ });
222
+ };
223
+ /**
224
+ * Transform the event payload.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * // Redact sensitive fields
229
+ * const redact = mapPayload((payload) => ({
230
+ * ...payload,
231
+ * password: "[REDACTED]",
232
+ * }))
233
+ * ```
234
+ */
235
+ const mapPayload = (fn) => {
236
+ return (event) => ({
237
+ ...event,
238
+ payload: fn(event.payload)
239
+ });
240
+ };
241
+ /**
242
+ * Transform the entire event.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * const transform = map((event) => ({
247
+ * ...event,
248
+ * timestamp: Date.now(),
249
+ * }))
250
+ * ```
251
+ */
252
+ const map = (fn) => {
253
+ return (event) => fn(event);
254
+ };
255
+ /**
256
+ * Tap into the event stream for side effects.
257
+ * Does not modify the event.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * const logger = tap((event) => console.log("Tracking:", event.name))
262
+ * ```
263
+ */
264
+ const tap = (fn) => {
265
+ return (event) => {
266
+ fn(event);
267
+ return event;
268
+ };
269
+ };
270
+
271
+ //#endregion
272
+ //#region src/internal/id.ts
273
+ /**
274
+ * Default ID generator.
275
+ */
276
+ const generateId = () => {
277
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
278
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
279
+ let result = "";
280
+ for (let i = 0; i < 21; i++) result += chars[Math.floor(Math.random() * 36)];
281
+ return result;
282
+ };
283
+
284
+ //#endregion
285
+ //#region src/transport.ts
286
+ /**
287
+ * Error thrown when a transport fails to send events.
288
+ */
289
+ var TransportError = class extends Error {
290
+ name = "TransportError";
291
+ transport;
292
+ retryable;
293
+ constructor(options) {
294
+ super(options.reason);
295
+ this.transport = options.transport;
296
+ this.retryable = options.retryable;
297
+ }
298
+ };
299
+
300
+ //#endregion
301
+ //#region src/tracker.ts
302
+ /**
303
+ * Tracker - main entry point with vanilla JS API.
304
+ * Uses Effect internally for batching, retry, and queue management.
305
+ *
306
+ * @since 1.0.0
307
+ */
308
+ /**
309
+ * Create a new tracker instance.
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * const tracker = createTracker({
314
+ * transports: [httpTransport],
315
+ * batchSize: 10,
316
+ * flushIntervalMs: 5000,
317
+ * })
318
+ *
319
+ * tracker.track("page_view", { page: "/home" })
320
+ *
321
+ * // Later...
322
+ * await tracker.shutdown()
323
+ * ```
324
+ */
325
+ const createTracker = (config, middleware) => {
326
+ const resolved = resolveConfig(config);
327
+ const mw = middleware ?? identity;
328
+ const generateId$1 = config.generateId ?? generateId;
329
+ const globalMetadata = config.metadata ?? {};
330
+ let runtime = null;
331
+ let isShutdown = false;
332
+ const ensureRuntime = () => {
333
+ if (isShutdown) throw new Error("Tracker has been shut down");
334
+ if (!runtime) runtime = createRuntime(resolved, mw, generateId$1, globalMetadata);
335
+ return runtime;
336
+ };
337
+ return {
338
+ track: (name, payload) => {
339
+ const rt = ensureRuntime();
340
+ const transformed = mw(createEvent(name, payload, {
341
+ id: generateId$1(),
342
+ metadata: { ...globalMetadata }
343
+ }));
344
+ if (transformed) rt.offer(transformed);
345
+ },
346
+ trackAsync: async (name, payload) => {
347
+ const rt = ensureRuntime();
348
+ const transformed = mw(createEvent(name, payload, {
349
+ id: generateId$1(),
350
+ metadata: { ...globalMetadata }
351
+ }));
352
+ if (transformed) await rt.offerAsync(transformed);
353
+ },
354
+ trackWith: (name, payload, metadata) => {
355
+ const rt = ensureRuntime();
356
+ const transformed = mw(createEvent(name, payload, {
357
+ id: generateId$1(),
358
+ metadata: {
359
+ ...globalMetadata,
360
+ ...metadata
361
+ }
362
+ }));
363
+ if (transformed) rt.offer(transformed);
364
+ },
365
+ flush: async () => {
366
+ if (!runtime || isShutdown) return;
367
+ await runtime.flush();
368
+ },
369
+ shutdown: async () => {
370
+ if (isShutdown) return;
371
+ isShutdown = true;
372
+ if (runtime) {
373
+ await runtime.shutdown();
374
+ runtime = null;
375
+ }
376
+ }
377
+ };
378
+ };
379
+ const createRuntime = (config, _middleware, _generateId, _globalMetadata) => {
380
+ const queueEffect = createQueue(config.queueCapacity, config.queueStrategy);
381
+ const scope = effect.Effect.runSync(effect.Scope.make());
382
+ const queue = effect.Effect.runSync(queueEffect);
383
+ const retrySchedule = effect.Schedule.exponential(effect.Duration.millis(config.retryDelayMs)).pipe(effect.Schedule.jittered, effect.Schedule.compose(effect.Schedule.recurs(config.retryAttempts)));
384
+ const dispatch = async (events) => {
385
+ if (events.length === 0) return;
386
+ const results = await Promise.allSettled(config.transports.map((transport) => dispatchToTransport(transport, events, retrySchedule)));
387
+ for (const result of results) if (result.status === "rejected") {
388
+ const error = result.reason;
389
+ if (config.onError) config.onError(error, events);
390
+ config.logger.error("Transport failed:", error.message);
391
+ }
392
+ };
393
+ const batchLoop = effect.Effect.gen(function* () {
394
+ yield* effect.Effect.forever(effect.Effect.gen(function* () {
395
+ const events = yield* effect.Effect.race(effect.Queue.takeBetween(queue, config.batchSize, config.batchSize), effect.Effect.sleep(effect.Duration.millis(config.flushIntervalMs)).pipe(effect.Effect.zipRight(effect.Queue.takeUpTo(queue, config.batchSize))));
396
+ if (events.length > 0) yield* effect.Effect.tryPromise({
397
+ try: () => dispatch([...events]),
398
+ catch: () => /* @__PURE__ */ new Error("Dispatch failed")
399
+ }).pipe(effect.Effect.catchAll(() => effect.Effect.void));
400
+ }));
401
+ });
402
+ const batchFiber = effect.Effect.runFork(batchLoop.pipe(effect.Effect.provideService(effect.Scope.Scope, scope)));
403
+ return {
404
+ offer: (event) => {
405
+ effect.Effect.runFork(effect.Queue.offer(queue, event));
406
+ },
407
+ offerAsync: (event) => effect.Effect.runPromise(effect.Queue.offer(queue, event)).then(() => void 0),
408
+ flush: async () => {
409
+ const events = await effect.Effect.runPromise(effect.Queue.takeAll(queue));
410
+ if (events.length > 0) await dispatch([...events]);
411
+ },
412
+ shutdown: async () => {
413
+ await effect.Effect.runPromise(effect.Fiber.interrupt(batchFiber));
414
+ const events = await effect.Effect.runPromise(effect.Queue.takeAll(queue));
415
+ if (events.length > 0) await dispatch([...events]);
416
+ await effect.Effect.runPromise(effect.Scope.close(scope, effect.Exit.void));
417
+ }
418
+ };
419
+ };
420
+ const createQueue = (capacity, strategy) => {
421
+ switch (strategy) {
422
+ case "bounded": return effect.Queue.bounded(capacity);
423
+ case "dropping": return effect.Queue.dropping(capacity);
424
+ case "sliding": return effect.Queue.sliding(capacity);
425
+ default: return effect.Queue.dropping(capacity);
426
+ }
427
+ };
428
+ const dispatchToTransport = async (transport, events, retrySchedule) => {
429
+ const effect$1 = effect.Effect.tryPromise({
430
+ try: () => transport.send(events),
431
+ catch: (error) => {
432
+ if (error instanceof TransportError) return error;
433
+ return new TransportError({
434
+ transport: transport.name,
435
+ reason: String(error),
436
+ retryable: true
437
+ });
438
+ }
439
+ }).pipe(effect.Effect.retry({
440
+ schedule: retrySchedule,
441
+ while: (err) => err.retryable
442
+ }));
443
+ await effect.Effect.runPromise(effect$1);
444
+ };
445
+
446
+ //#endregion
447
+ exports.TransportError = TransportError;
448
+ exports.addMetadata = addMetadata;
449
+ exports.addMetadataFrom = addMetadataFrom;
450
+ exports.compose = compose;
451
+ exports.consoleLogger = consoleLogger;
452
+ exports.createEvent = createEvent;
453
+ exports.createMinLevelLogger = createMinLevelLogger;
454
+ exports.createTracker = createTracker;
455
+ exports.defaults = defaults;
456
+ exports.filter = filter;
457
+ exports.identity = identity;
458
+ exports.map = map;
459
+ exports.mapName = mapName;
460
+ exports.mapPayload = mapPayload;
461
+ exports.noopLogger = noopLogger;
462
+ exports.resolveConfig = resolveConfig;
463
+ exports.tap = tap;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Trashlytics - A lightweight event tracking library.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { createTracker, TransportError } from "trashlytics"
7
+ *
8
+ * const tracker = createTracker({
9
+ * transports: [{
10
+ * name: "http",
11
+ * send: async (events) => {
12
+ * const response = await fetch("/analytics", {
13
+ * method: "POST",
14
+ * body: JSON.stringify(events),
15
+ * })
16
+ * if (!response.ok) {
17
+ * throw new TransportError({
18
+ * transport: "http",
19
+ * reason: `HTTP ${response.status}`,
20
+ * retryable: response.status >= 500,
21
+ * })
22
+ * }
23
+ * },
24
+ * }],
25
+ * batchSize: 10,
26
+ * flushIntervalMs: 5000,
27
+ * })
28
+ *
29
+ * tracker.track("page_view", { page: "/home" })
30
+ * tracker.track("button_click", { buttonId: "signup" })
31
+ *
32
+ * // Graceful shutdown
33
+ * await tracker.shutdown()
34
+ * ```
35
+ *
36
+ * @since 1.0.0
37
+ */
38
+ export type { QueueStrategy, ResolvedConfig, TrackerConfig } from "./config.js";
39
+ export { defaults, resolveConfig } from "./config.js";
40
+ export type { Event, EventOptions } from "./event.js";
41
+ export { createEvent } from "./event.js";
42
+ export type { Logger, LogLevel } from "./logger.js";
43
+ export { consoleLogger, createMinLevelLogger, noopLogger } from "./logger.js";
44
+ export type { Middleware } from "./middleware.js";
45
+ export { addMetadata, addMetadataFrom, compose, filter, identity, map, mapName, mapPayload, tap, } from "./middleware.js";
46
+ export type { Tracker } from "./tracker.js";
47
+ export { createTracker } from "./tracker.js";
48
+ export type { Transport } from "./transport.js";
49
+ export { TransportError } from "./transport.js";
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Trashlytics - A lightweight event tracking library.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { createTracker, TransportError } from "trashlytics"
7
+ *
8
+ * const tracker = createTracker({
9
+ * transports: [{
10
+ * name: "http",
11
+ * send: async (events) => {
12
+ * const response = await fetch("/analytics", {
13
+ * method: "POST",
14
+ * body: JSON.stringify(events),
15
+ * })
16
+ * if (!response.ok) {
17
+ * throw new TransportError({
18
+ * transport: "http",
19
+ * reason: `HTTP ${response.status}`,
20
+ * retryable: response.status >= 500,
21
+ * })
22
+ * }
23
+ * },
24
+ * }],
25
+ * batchSize: 10,
26
+ * flushIntervalMs: 5000,
27
+ * })
28
+ *
29
+ * tracker.track("page_view", { page: "/home" })
30
+ * tracker.track("button_click", { buttonId: "signup" })
31
+ *
32
+ * // Graceful shutdown
33
+ * await tracker.shutdown()
34
+ * ```
35
+ *
36
+ * @since 1.0.0
37
+ */
38
+ export type { QueueStrategy, ResolvedConfig, TrackerConfig } from "./config.js";
39
+ export { defaults, resolveConfig } from "./config.js";
40
+ export type { Event, EventOptions } from "./event.js";
41
+ export { createEvent } from "./event.js";
42
+ export type { Logger, LogLevel } from "./logger.js";
43
+ export { consoleLogger, createMinLevelLogger, noopLogger } from "./logger.js";
44
+ export type { Middleware } from "./middleware.js";
45
+ export { addMetadata, addMetadataFrom, compose, filter, identity, map, mapName, mapPayload, tap, } from "./middleware.js";
46
+ export type { Tracker } from "./tracker.js";
47
+ export { createTracker } from "./tracker.js";
48
+ export type { Transport } from "./transport.js";
49
+ export { TransportError } from "./transport.js";
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAGH,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEtD,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9E,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EACL,WAAW,EACX,eAAe,EACf,OAAO,EACP,MAAM,EACN,QAAQ,EACR,GAAG,EACH,OAAO,EACP,UAAU,EACV,GAAG,GACJ,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG7C,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}