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/README.md ADDED
@@ -0,0 +1,319 @@
1
+ # trashlytics
2
+
3
+ A lightweight, generic event tracking library with built-in batching, retry logic, and middleware support. Uses [Effect](https://effect.website) internally for robust async handling, but exposes a simple vanilla JavaScript API.
4
+
5
+ ## Features
6
+
7
+ - **Simple API** - Just functions and Promises, no framework knowledge required
8
+ - **Generic Events** - Track any payload type with full TypeScript support
9
+ - **Multiple Transports** - Fan-out events to multiple destinations concurrently
10
+ - **Batching** - Configurable batch size and flush interval
11
+ - **Retry Logic** - Exponential backoff with jitter for failed sends
12
+ - **Middleware** - Composable event transformations (filter, enrich, transform)
13
+ - **Queue Strategies** - Bounded, dropping, or sliding window queues
14
+ - **Universal** - Works in browser and Node.js environments
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install trashlytics effect
20
+ # or
21
+ pnpm add trashlytics effect
22
+ # or
23
+ yarn add trashlytics effect
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { createTracker, TransportError } from "trashlytics"
30
+
31
+ // 1. Create a tracker with your transport
32
+ const tracker = createTracker({
33
+ transports: [{
34
+ name: "http",
35
+ send: async (events) => {
36
+ const response = await fetch("/api/analytics", {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify(events),
40
+ })
41
+ if (!response.ok) {
42
+ throw new TransportError({
43
+ transport: "http",
44
+ reason: `HTTP ${response.status}`,
45
+ retryable: response.status >= 500,
46
+ })
47
+ }
48
+ },
49
+ }],
50
+ batchSize: 10,
51
+ flushIntervalMs: 5000,
52
+ })
53
+
54
+ // 2. Track events (fire-and-forget)
55
+ tracker.track("page_view", { page: "/home" })
56
+ tracker.track("button_click", { buttonId: "signup" })
57
+
58
+ // 3. Graceful shutdown when done
59
+ await tracker.shutdown()
60
+ ```
61
+
62
+ ## Configuration
63
+
64
+ ```typescript
65
+ import { createTracker } from "trashlytics"
66
+
67
+ const tracker = createTracker({
68
+ // Required: array of transports
69
+ transports: [httpTransport, consoleTransport],
70
+
71
+ // Batching
72
+ batchSize: 10, // Events per batch (default: 10)
73
+ flushIntervalMs: 5000, // Max time before flush in ms (default: 5000)
74
+
75
+ // Queue
76
+ queueCapacity: 1000, // Max queued events (default: 1000)
77
+ queueStrategy: "dropping", // "bounded" | "dropping" | "sliding"
78
+
79
+ // Retry
80
+ retryAttempts: 3, // Max retry attempts (default: 3)
81
+ retryDelayMs: 1000, // Base delay for backoff in ms (default: 1000)
82
+
83
+ // Shutdown
84
+ shutdownTimeoutMs: 30000, // Max shutdown wait in ms (default: 30000)
85
+
86
+ // ID Generation
87
+ generateId: () => crypto.randomUUID(), // Custom ID generator
88
+
89
+ // Global Metadata (added to all events)
90
+ metadata: {
91
+ appVersion: "1.0.0",
92
+ environment: "production",
93
+ },
94
+
95
+ // Error Callback (called after all retries exhausted)
96
+ onError: (error, events) => {
97
+ console.error(`Failed to send ${events.length} events:`, error.message)
98
+ },
99
+ })
100
+ ```
101
+
102
+ ## Middleware
103
+
104
+ Middleware allows you to transform, filter, or enrich events before they're sent. Middleware functions receive an event and return a transformed event (or `null` to filter it out).
105
+
106
+ ### Built-in Middleware
107
+
108
+ ```typescript
109
+ import { createTracker, compose, filter, addMetadata, mapName, tap } from "trashlytics"
110
+
111
+ // Compose multiple middlewares (executed left to right)
112
+ const middleware = compose(
113
+ // Filter out internal events
114
+ filter((event) => !event.name.startsWith("_")),
115
+
116
+ // Add static metadata
117
+ addMetadata({
118
+ appVersion: "1.0.0",
119
+ platform: "web",
120
+ }),
121
+
122
+ // Prefix event names
123
+ mapName((name) => `app.${name}`),
124
+
125
+ // Side effects (logging, etc.)
126
+ tap((event) => console.log("Tracking:", event.name)),
127
+ )
128
+
129
+ // Use with tracker
130
+ const tracker = createTracker({ transports }, middleware)
131
+ ```
132
+
133
+ ### Available Middleware Functions
134
+
135
+ | Function | Description |
136
+ |----------|-------------|
137
+ | `filter(predicate)` | Filter events based on a predicate |
138
+ | `addMetadata(obj)` | Add static metadata to all events |
139
+ | `addMetadataFrom(fn)` | Add dynamic metadata based on event |
140
+ | `mapName(fn)` | Transform event name |
141
+ | `mapPayload(fn)` | Transform event payload |
142
+ | `map(fn)` | Transform entire event |
143
+ | `tap(fn)` | Side effects without modifying event |
144
+ | `compose(...middlewares)` | Compose multiple middlewares |
145
+ | `identity` | Pass-through middleware |
146
+
147
+ ### Custom Middleware
148
+
149
+ ```typescript
150
+ import type { Middleware } from "trashlytics"
151
+
152
+ // Middleware is just a function: Event -> Event | null
153
+ const redactPasswords: Middleware = (event) => ({
154
+ ...event,
155
+ payload: {
156
+ ...event.payload,
157
+ password: event.payload.password ? "[REDACTED]" : undefined,
158
+ },
159
+ })
160
+ ```
161
+
162
+ ## Multiple Transports
163
+
164
+ Send events to multiple destinations simultaneously:
165
+
166
+ ```typescript
167
+ import { createTracker, TransportError } from "trashlytics"
168
+
169
+ const httpTransport = {
170
+ name: "http",
171
+ send: async (events) => {
172
+ await fetch("/api/analytics", {
173
+ method: "POST",
174
+ body: JSON.stringify(events),
175
+ })
176
+ },
177
+ }
178
+
179
+ const consoleTransport = {
180
+ name: "console",
181
+ send: async (events) => {
182
+ console.log("[Analytics]", events)
183
+ },
184
+ }
185
+
186
+ // All transports receive events concurrently
187
+ const tracker = createTracker({
188
+ transports: [httpTransport, consoleTransport],
189
+ })
190
+ ```
191
+
192
+ ## Custom Transport
193
+
194
+ Implement the `Transport` interface:
195
+
196
+ ```typescript
197
+ import type { Transport } from "trashlytics"
198
+ import { TransportError } from "trashlytics"
199
+
200
+ const myTransport: Transport = {
201
+ name: "my-analytics",
202
+ send: async (events) => {
203
+ try {
204
+ await myAnalyticsSDK.track(events)
205
+ } catch (error) {
206
+ throw new TransportError({
207
+ transport: "my-analytics",
208
+ reason: String(error),
209
+ retryable: true, // Set false for non-retryable errors
210
+ })
211
+ }
212
+ },
213
+ }
214
+ ```
215
+
216
+ ## Queue Strategies
217
+
218
+ Control behavior when the event queue is full:
219
+
220
+ | Strategy | Behavior |
221
+ |----------|----------|
222
+ | `"bounded"` | Back-pressure - blocks until space is available |
223
+ | `"dropping"` | Drops new events when queue is full (default) |
224
+ | `"sliding"` | Drops oldest events when queue is full |
225
+
226
+ ```typescript
227
+ const tracker = createTracker({
228
+ transports,
229
+ queueCapacity: 500,
230
+ queueStrategy: "sliding", // Keep most recent events
231
+ })
232
+ ```
233
+
234
+ ## API Reference
235
+
236
+ ### Tracker
237
+
238
+ ```typescript
239
+ interface Tracker {
240
+ // Track an event (fire-and-forget)
241
+ track<T>(name: string, payload: T): void
242
+
243
+ // Track and wait for queue
244
+ trackAsync<T>(name: string, payload: T): Promise<void>
245
+
246
+ // Track with additional metadata
247
+ trackWith<T>(name: string, payload: T, metadata: Record<string, unknown>): void
248
+
249
+ // Flush all queued events immediately
250
+ flush(): Promise<void>
251
+
252
+ // Graceful shutdown (flush + cleanup)
253
+ shutdown(): Promise<void>
254
+ }
255
+ ```
256
+
257
+ ### Event
258
+
259
+ ```typescript
260
+ interface Event<T = unknown> {
261
+ id: string
262
+ name: string
263
+ timestamp: number
264
+ payload: T
265
+ metadata: Record<string, unknown>
266
+ }
267
+ ```
268
+
269
+ ### TransportError
270
+
271
+ ```typescript
272
+ class TransportError extends Error {
273
+ transport: string // Transport name
274
+ retryable: boolean // Whether to retry
275
+ }
276
+ ```
277
+
278
+ ## Browser Tips
279
+
280
+ ### Beacon API Transport
281
+
282
+ For reliable delivery on page unload:
283
+
284
+ ```typescript
285
+ const beaconTransport: Transport = {
286
+ name: "beacon",
287
+ send: async (events) => {
288
+ const success = navigator.sendBeacon("/api/analytics", JSON.stringify(events))
289
+ if (!success) {
290
+ // Fallback to fetch
291
+ await fetch("/api/analytics", {
292
+ method: "POST",
293
+ body: JSON.stringify(events),
294
+ keepalive: true,
295
+ })
296
+ }
297
+ },
298
+ }
299
+ ```
300
+
301
+ ### Page Lifecycle Events
302
+
303
+ Flush events when the page is hidden or unloaded:
304
+
305
+ ```typescript
306
+ document.addEventListener("visibilitychange", () => {
307
+ if (document.visibilityState === "hidden") {
308
+ tracker.flush()
309
+ }
310
+ })
311
+
312
+ window.addEventListener("pagehide", () => {
313
+ tracker.flush()
314
+ })
315
+ ```
316
+
317
+ ## License
318
+
319
+ MIT
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Configuration types and defaults.
3
+ *
4
+ * @since 1.0.0
5
+ */
6
+ import type { Event } from "./event.js";
7
+ import type { Logger } from "./logger.js";
8
+ import type { Transport, TransportError } from "./transport.js";
9
+ /**
10
+ * Queue overflow strategy.
11
+ *
12
+ * - `bounded`: Back-pressure (blocks when full)
13
+ * - `dropping`: Drops new events when full
14
+ * - `sliding`: Drops oldest events when full
15
+ */
16
+ export type QueueStrategy = "bounded" | "dropping" | "sliding";
17
+ /**
18
+ * Configuration for the Tracker.
19
+ */
20
+ export interface TrackerConfig {
21
+ /** Array of transports to send events to */
22
+ readonly transports: readonly Transport[];
23
+ /**
24
+ * Custom ID generator for events.
25
+ * If not provided, uses crypto.randomUUID or a fallback.
26
+ */
27
+ readonly generateId?: () => string;
28
+ /**
29
+ * Number of events to batch before sending.
30
+ * @default 10
31
+ */
32
+ readonly batchSize?: number;
33
+ /**
34
+ * Maximum time (ms) to wait before flushing events.
35
+ * @default 5000
36
+ */
37
+ readonly flushIntervalMs?: number;
38
+ /**
39
+ * Maximum number of events to queue.
40
+ * @default 1000
41
+ */
42
+ readonly queueCapacity?: number;
43
+ /**
44
+ * Strategy when queue is full.
45
+ * @default "dropping"
46
+ */
47
+ readonly queueStrategy?: QueueStrategy;
48
+ /**
49
+ * Number of retry attempts for failed sends.
50
+ * @default 3
51
+ */
52
+ readonly retryAttempts?: number;
53
+ /**
54
+ * Base delay (ms) for exponential backoff.
55
+ * @default 1000
56
+ */
57
+ readonly retryDelayMs?: number;
58
+ /**
59
+ * Timeout (ms) for graceful shutdown.
60
+ * @default 30000
61
+ */
62
+ readonly shutdownTimeoutMs?: number;
63
+ /**
64
+ * Global metadata added to all events.
65
+ */
66
+ readonly metadata?: Record<string, unknown>;
67
+ /**
68
+ * Callback for transport errors.
69
+ * Called after all retries are exhausted.
70
+ */
71
+ readonly onError?: (error: TransportError, events: readonly Event[]) => void;
72
+ /**
73
+ * Custom logger for library output.
74
+ * Use `noopLogger` to disable all logging.
75
+ * @default consoleLogger
76
+ */
77
+ readonly logger?: Logger;
78
+ }
79
+ /**
80
+ * Default configuration values.
81
+ */
82
+ export declare const defaults: {
83
+ batchSize: number;
84
+ flushIntervalMs: number;
85
+ queueCapacity: number;
86
+ queueStrategy: "dropping";
87
+ retryAttempts: number;
88
+ retryDelayMs: number;
89
+ shutdownTimeoutMs: number;
90
+ };
91
+ /**
92
+ * Resolved configuration with all defaults applied.
93
+ */
94
+ export type ResolvedConfig = Required<Omit<TrackerConfig, "generateId" | "metadata" | "onError" | "logger">> & Pick<TrackerConfig, "generateId" | "metadata" | "onError"> & {
95
+ readonly logger: Logger;
96
+ };
97
+ /**
98
+ * Resolves a partial config with defaults.
99
+ */
100
+ export declare const resolveConfig: (config: TrackerConfig) => ResolvedConfig;
101
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAE/D;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;IAE1C;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC;IAEnC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAE5B;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAElC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IAEvC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAEpC;;OAEG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE5C;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,KAAK,EAAE,KAAK,IAAI,CAAC;IAE7E;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,eAAO,MAAM,QAAQ;;;;;;;;CAQpB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,CACnC,IAAI,CAAC,aAAa,EAAE,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC,CACtE,GACC,IAAI,CAAC,aAAa,EAAE,YAAY,GAAG,UAAU,GAAG,SAAS,CAAC,GAAG;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,aAAa,KAAG,cAerD,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Event types and helpers.
3
+ *
4
+ * @since 1.0.0
5
+ */
6
+ /**
7
+ * Represents a tracked event with generic payload.
8
+ */
9
+ export interface Event<T = unknown> {
10
+ readonly id: string;
11
+ readonly name: string;
12
+ readonly timestamp: number;
13
+ readonly payload: T;
14
+ readonly metadata: Record<string, unknown>;
15
+ }
16
+ /**
17
+ * Options for creating an event.
18
+ */
19
+ export interface EventOptions {
20
+ readonly id: string;
21
+ readonly metadata?: Record<string, unknown>;
22
+ readonly timestamp?: number;
23
+ }
24
+ /**
25
+ * Creates a new event with the given name, payload, and options.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const event = createEvent("page_view", { page: "/home" }, {
30
+ * id: "abc123",
31
+ * metadata: { userId: "user_1" }
32
+ * })
33
+ * ```
34
+ */
35
+ export declare const createEvent: <T>(name: string, payload: T, options: EventOptions) => Event<T>;
36
+ //# sourceMappingURL=event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../src/event.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,KAAK,CAAC,CAAC,GAAG,OAAO;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,EAC3B,MAAM,MAAM,EACZ,SAAS,CAAC,EACV,SAAS,YAAY,KACpB,KAAK,CAAC,CAAC,CAMR,CAAC"}