@borisch/snitch 1.0.1 → 1.0.2

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.
Files changed (2) hide show
  1. package/README.md +288 -28
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -11,12 +11,23 @@ npm install @borisch/snitch
11
11
  ## Quick Start
12
12
 
13
13
  ```ts
14
- import { snitch, sessionPlugin, scrollPlugin, beaconTransportPlugin } from '@borisch/snitch'
14
+ import {
15
+ snitch,
16
+ sessionPlugin,
17
+ launchPlugin,
18
+ scrollPlugin,
19
+ locationPlugin,
20
+ beaconTransportPlugin,
21
+ debugLoggerPlugin,
22
+ } from '@borisch/snitch'
15
23
 
16
24
  const captureEvent = snitch(
17
25
  sessionPlugin(),
18
- scrollPlugin({ threshold: 25 }),
19
- beaconTransportPlugin({ url: 'https://analytics.example.com/collect' })
26
+ launchPlugin(),
27
+ scrollPlugin(),
28
+ locationPlugin({ captureLocationChange: true }),
29
+ beaconTransportPlugin({ hostname: 'analytics.example.com' }),
30
+ debugLoggerPlugin(),
20
31
  )
21
32
 
22
33
  // Manually capture events
@@ -25,43 +36,283 @@ captureEvent('button_click', { buttonId: 'signup' })
25
36
 
26
37
  The `snitch()` function accepts any number of plugins and returns a `captureEvent` function. Plugins can:
27
38
 
28
- - **Provide event parameters** (automatically attached to every event)
39
+ - **Provide event parameters** automatically attached to every event
29
40
  - **Emit events** on their own (e.g. scroll milestones, page views)
30
- - **Transport events** to a backend (Beacon API, fetch, VK Bridge, etc.)
41
+ - **Transport events** to a backend
31
42
  - **Intercept events** before they are sent
43
+ - **Expose mixins** — additional methods attached to the `captureEvent` function
32
44
 
33
45
  ## Plugins
34
46
 
35
- | Plugin | Factory | Description |
36
- | ------------------ | ------------------------ | ---------------------------------------------------------- |
37
- | Session | `sessionPlugin()` | Generates a unique session ID, attaches it to every event |
38
- | Launch | `launchPlugin()` | Captures a `launch` event on initialization |
39
- | Scroll | `scrollPlugin(opts)` | Tracks scroll depth milestones (default: 25/50/75/100%) |
40
- | Location | `locationPlugin()` | Attaches page URL and referrer to every event |
41
- | Engagement | `engagementPlugin(opts)` | Tracks time spent on page, emits engagement events |
42
- | Screens | `screenPlugin()` | Tracks screen/page view events |
43
- | Exceptions | `exceptionsPlugin()` | Captures unhandled errors and unhandled promise rejections |
44
- | Web Vitals | `webVitalsPlugin()` | Reports Core Web Vitals (LCP, FID, CLS, etc.) |
45
- | Flag | `flagPlugin(opts)` | Attaches a static key-value flag to every event |
46
- | User Agent | `useragentPlugin()` | Attaches `navigator.userAgent` to every event |
47
- | Debug Logger | `debugLoggerPlugin()` | Logs all events to the console (for development) |
48
- | VK Mini App Launch | `vkmaLaunchPlugin()` | Parses VK Mini App launch parameters |
47
+ ### `sessionPlugin()`
48
+
49
+ Manages user sessions using `localStorage`. A new session starts when:
50
+
51
+ - No previous session exists
52
+ - The previous session has been inactive for 30+ minutes
53
+ - UTM parameters are present in the URL
54
+
55
+ If a session expires between events, a new session is started automatically before the next event is sent.
56
+
57
+ Emits: `sessionStart`
58
+
59
+ Attaches to every event:
60
+ | Param | Description |
61
+ |-------|-------------|
62
+ | `sid` | Unique session ID |
63
+ | `scnt` | Total session count for this device |
64
+ | `set` | Milliseconds since session started |
65
+ | `sutm` | Compact UTM parameters from the URL that started the session |
66
+
67
+ ---
68
+
69
+ ### `launchPlugin()`
70
+
71
+ Captures a `launch` event when the tracker initializes. Records whether the page runs inside an iframe.
72
+
73
+ Emits: `launch` with `{ ifr: "true" | "false" }`
74
+
75
+ Attaches to every event:
76
+ | Param | Description |
77
+ |-------|-------------|
78
+ | `lid` | Unique launch ID (generated per `snitch()` call) |
79
+ | `ref` | `document.referrer` at initialization time |
80
+
81
+ ---
82
+
83
+ ### `scrollPlugin()`
84
+
85
+ Tracks scroll depth. Emits events when the user scrolls past depth milestones (25%, 50%, 75%, 100%). The scroll depth cache resets whenever a `locationChange` or `screenChange` event occurs, so milestones are tracked per-page.
86
+
87
+ Emits: `scroll` with `{ depthPercent: number }`
88
+
89
+ ---
90
+
91
+ ### `locationPlugin(options)`
92
+
93
+ Tracks the current page URL and optionally emits events on URL changes (SPA navigation, `pushState`, etc.).
94
+
95
+ **Options:**
96
+ | Option | Type | Description |
97
+ |--------|------|-------------|
98
+ | `captureLocationChange` | `boolean` | Whether to listen for URL changes and emit events |
99
+ | `getLocation` | `() => string` | Custom location getter (defaults to `window.location.href`) |
100
+
101
+ Emits (when `captureLocationChange` is `true`): `locationChange` with `{ phref: string }` (previous URL)
102
+
103
+ Attaches to every event:
104
+ | Param | Description |
105
+ |-------|-------------|
106
+ | `href` | Current page URL (truncated to 500 characters) |
107
+
108
+ ---
109
+
110
+ ### `engagementPlugin(options?)`
111
+
112
+ Periodically emits engagement events while the page is visible. Events are suppressed when the tab is hidden (`document.hidden === true`).
113
+
114
+ **Options:**
115
+ | Option | Type | Default | Description |
116
+ |--------|------|---------|-------------|
117
+ | `engagementTrackingIntervalMsec` | `number` | `10000` | Interval in milliseconds between engagement pings |
118
+
119
+ Emits: `engage` (at configured interval, only when tab is visible)
120
+
121
+ ---
122
+
123
+ ### `screenPlugin(initialScreen)`
124
+
125
+ Tracks screen/page views within an app. Maintains current and previous screen state.
126
+
127
+ **Options:**
128
+ | Option | Type | Description |
129
+ |--------|------|-------------|
130
+ | `screenType` | `string` | Type/category of the initial screen |
131
+ | `screenId` | `string?` | Optional screen identifier |
132
+
133
+ To change screens, call `captureEvent('screenChange', { screenType: 'catalog', screenId: 'page2' })`. The plugin automatically injects previous screen params and removes the raw `screenType`/`screenId` from the event payload.
134
+
135
+ Attaches to every event:
136
+ | Param | Description |
137
+ |-------|-------------|
138
+ | `sct` | Current screen type |
139
+ | `scid` | Current screen ID (or `""`) |
140
+
141
+ Attaches to `screenChange` events:
142
+ | Param | Description |
143
+ |-------|-------------|
144
+ | `psct` | Previous screen type |
145
+ | `pscid` | Previous screen ID (or `""`) |
146
+
147
+ ---
148
+
149
+ ### `exceptionsPlugin()`
150
+
151
+ Captures unhandled errors and promise rejections globally.
152
+
153
+ Emits:
154
+
155
+ - `uncaughtError` with `{ message, filename, lineno, colno, error }`
156
+ - `unhandledRejection` with `{ reason }`
157
+
158
+ ---
159
+
160
+ ### `webVitalsPlugin()`
161
+
162
+ Reports [Core Web Vitals](https://web.dev/vitals/) using the `web-vitals` library. Tracks CLS, FID, LCP, TTFB, and FCP.
163
+
164
+ Emits: `webVital` with `{ name, value, delta, metricId }`
165
+
166
+ ---
167
+
168
+ ### `flagPlugin(options)`
169
+
170
+ Feature flag evaluation plugin. Adds `getFlag()` and `getFlags()` methods to the `captureEvent` function via mixins.
171
+
172
+ **Options:**
173
+ | Option | Type | Description |
174
+ |--------|------|-------------|
175
+ | `flagApiEndpoint` | `string` | URL of the flag evaluation API |
176
+ | `userIdResolver` | `() => string \| null \| undefined` | Optional custom user ID resolver |
177
+
178
+ User ID is resolved in order: custom resolver → VK user ID from URL → Top Mail.ru counter cookie → auto-generated anonymous ID (persisted in `localStorage`).
179
+
180
+ **Usage:**
181
+
182
+ ```ts
183
+ const captureEvent = snitch(flagPlugin({ flagApiEndpoint: 'https://flags.example.com/api' })) as any
184
+
185
+ const flag = await captureEvent.getFlag('new-feature')
186
+ // { flagKey: 'new-feature', match: true, variant: 'control', attachment: '...' }
187
+
188
+ const flags = await captureEvent.getFlags(['feature-a', 'feature-b'])
189
+ ```
190
+
191
+ Emits:
192
+
193
+ - `flagEvaluationComplete` with full evaluation response
194
+ - `flagEvaluationFailed` with `{ flagKey, errorMessage }`
195
+
196
+ ---
197
+
198
+ ### `useragentPlugin()`
199
+
200
+ Attaches the browser user agent string to every event.
201
+
202
+ Attaches to every event:
203
+ | Param | Description |
204
+ |-------|-------------|
205
+ | `ua` | `navigator.userAgent` |
206
+
207
+ ---
208
+
209
+ ### `debugLoggerPlugin()`
210
+
211
+ Development helper. Logs every event to the browser console with timestamps and time deltas between events. When the event has a non-empty payload, it is also rendered via `console.table()`.
212
+
213
+ **Silent by default.** To enable, set a `localStorage` flag:
214
+
215
+ ```js
216
+ localStorage.setItem('snitch:debug', 'true')
217
+ ```
218
+
219
+ The flag is read once when `debugLoggerPlugin()` is called. To disable, remove the flag and reload:
220
+
221
+ ```js
222
+ localStorage.removeItem('snitch:debug')
223
+ ```
224
+
225
+ ---
49
226
 
50
227
  ## Transports
51
228
 
52
- | Transport | Factory | Description |
53
- | ----------- | -------------------------------- | ------------------------------------------------------------------------------- |
54
- | Beacon | `beaconTransportPlugin(opts)` | Sends events as URL query params via `navigator.sendBeacon()` |
55
- | S2S | `s2sTransportPlugin(opts)` | Sends events as URL query params via `fetch()` GET (for server-side / Node 18+) |
56
- | Top Mail.ru | `topmailruTransportPlugin(opts)` | Sends events to Top Mail.ru counter |
57
- | VK Bridge | `vkBridgeTransportPlugin(opts)` | Sends events via VK Bridge |
229
+ ### `beaconTransportPlugin(options?)`
230
+
231
+ Sends events via [`navigator.sendBeacon()`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon). All event data is encoded as URL query parameters designed for CDN log-based analytics where request URLs are parsed from access logs.
232
+
233
+ **Options:**
234
+ | Option | Type | Default | Description |
235
+ |--------|------|---------|-------------|
236
+ | `hostname` | `string` | `window.location.hostname` | Target hostname |
237
+ | `path` | `string` | `/_snitch` | URL path |
238
+
239
+ Requests are sent to: `{protocol}//{hostname}{path}?event={name}&...params`
240
+
241
+ ---
242
+
243
+ ### `s2sTransportPlugin(options)`
244
+
245
+ Sends events via `fetch()` GET requests over HTTPS. Fire-and-forget (errors are silently caught). Designed for server-side environments (Node 18+, Cloudflare Workers) or any environment with `fetch`.
246
+
247
+ **Options:**
248
+ | Option | Type | Default | Description |
249
+ |--------|------|---------|-------------|
250
+ | `hostname` | `string` | _required_ | Target hostname |
251
+ | `path` | `string` | `/_snitch` | URL path |
252
+ | `s2sToken` | `string` | — | Optional auth token (sent as a query parameter) |
253
+
254
+ Requests are sent to: `https://{hostname}{path}?event={name}&...params[&s2sToken=...]`
255
+
256
+ ---
257
+
258
+ ### `topmailruTransportPlugin(counterId, userIdResolver?)`
259
+
260
+ Sends events to [Top Mail.ru](https://top.mail.ru/) analytics counter by pushing to the `window._tmr` queue.
261
+
262
+ **Parameters:**
263
+ | Param | Type | Description |
264
+ |-------|------|-------------|
265
+ | `counterId` | `string` | Top Mail.ru counter ID (required) |
266
+ | `userIdResolver` | `() => string \| null \| undefined` | Optional custom user ID resolver |
267
+
268
+ User ID resolution order: custom resolver → TMR counter cookie → auto-generated anonymous ID.
269
+
270
+ ---
271
+
272
+ ### `vkBridgeTransportPlugin()`
273
+
274
+ Sends events via [VK Bridge](https://dev.vk.com/mini-apps/bridge) for VK Mini Apps. Extracts `vk_user_id` from the URL. Each event triggers two VK Bridge calls: `VKWebAppTrackEvent` and `VKWebAppSendCustomEvent`. All param values are coerced to strings to work around iOS VK Bridge limitations.
275
+
276
+ ---
277
+
278
+ ## Platform-Specific Plugins
279
+
280
+ ### `vkmaLaunchPlugin()`
281
+
282
+ A VK Mini Apps variant of `launchPlugin`. Parses VK Mini App launch parameters from the URL (`vk_user_id`, `vk_app_id`, `vk_platform`, `vk_ref`, etc.).
283
+
284
+ Emits: `launch` (with iframe flag + VKMA params), `mt_internal_launch`
285
+
286
+ Attaches to every event:
287
+ | Param | Description |
288
+ |-------|-------------|
289
+ | `lid` | Unique launch ID |
290
+ | `ref` | `document.referrer` |
291
+ | `mauid` | VK user ID |
292
+ | `maaid` | VK app ID |
293
+ | `malang` | VK language |
294
+ | `mac` | VK access token settings |
295
+ | `map` | VK platform |
296
+ | `maref` | VK ref |
297
+
298
+ ---
58
299
 
59
300
  ## Types
60
301
 
61
302
  All public types are exported for TypeScript consumers:
62
303
 
63
304
  ```ts
64
- import type { Plugin, EventTransport, TrackerEventPayload } from '@borisch/snitch'
305
+ import type {
306
+ Plugin,
307
+ EventTransport,
308
+ EventSource,
309
+ EventPayloadParamsProvider,
310
+ InitializationHandler,
311
+ BeforeCaptureEventHandler,
312
+ MixinProvider,
313
+ TrackerEventPayload,
314
+ EventHandler,
315
+ } from '@borisch/snitch'
65
316
  ```
66
317
 
67
318
  ## Writing a Custom Plugin
@@ -73,12 +324,21 @@ import type { Plugin } from '@borisch/snitch'
73
324
 
74
325
  function myPlugin(): Plugin {
75
326
  return {
327
+ // Attach params to every event
76
328
  getEventPayloadParams() {
77
329
  return { customParam: 'value' }
78
330
  },
331
+ // React to events before transport
332
+ beforeCaptureEvent(eventName, eventParams) {
333
+ // filter, modify, log, etc.
334
+ },
335
+ // Transport events
79
336
  sendEvent(eventName, eventParams) {
80
- // custom transport logic
81
- }
337
+ fetch('/analytics', {
338
+ method: 'POST',
339
+ body: JSON.stringify({ eventName, ...eventParams }),
340
+ })
341
+ },
82
342
  }
83
343
  }
84
344
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@borisch/snitch",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Modular analytics tracking library with pluggable transports",
5
5
  "keywords": [
6
6
  "analytics",