@watchupltd/browser 0.1.0 → 0.1.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.
package/dist/index.d.mts CHANGED
@@ -1,3 +1,11 @@
1
+ interface WatchupUser {
2
+ /** Your app's internal user ID — the only required field. */
3
+ id: string | number;
4
+ email?: string;
5
+ name?: string;
6
+ /** Any extra key/value pairs you want attached to events. */
7
+ [key: string]: unknown;
8
+ }
1
9
  interface WatchupOptions {
2
10
  /**
3
11
  * Your project's **public** API key from the Watchup dashboard.
@@ -46,6 +54,7 @@ interface TracePayload {
46
54
  environment?: string;
47
55
  release?: string;
48
56
  meta?: Record<string, unknown>;
57
+ user?: WatchupUser;
49
58
  }
50
59
  interface ErrorPayload {
51
60
  message: string;
@@ -56,6 +65,7 @@ interface ErrorPayload {
56
65
  timestamp: string;
57
66
  environment?: string;
58
67
  release?: string;
68
+ user?: WatchupUser;
59
69
  }
60
70
  interface EventPayload {
61
71
  name: string;
@@ -67,17 +77,81 @@ interface IngestBatch {
67
77
  errors?: ErrorPayload[];
68
78
  events?: EventPayload[];
69
79
  }
80
+ interface WebAnalyticsPayload {
81
+ /** URL path, e.g. "/pricing". */
82
+ path: string;
83
+ /** Hostname of the tracked site, e.g. "example.com". */
84
+ hostname: string;
85
+ /** Full referrer URL (document.referrer). */
86
+ referrer?: string;
87
+ /** document.title at tracking time. */
88
+ title?: string;
89
+ /** Screen width in CSS pixels. */
90
+ screen_w?: number;
91
+ /** Screen height in CSS pixels. */
92
+ screen_h?: number;
93
+ /** navigator.language, e.g. "en-GB". */
94
+ lang?: string;
95
+ /** Intl.DateTimeFormat().resolvedOptions().timeZone */
96
+ timezone?: string;
97
+ /** UTM parameters parsed from the current URL's query string. */
98
+ utm_source?: string;
99
+ utm_medium?: string;
100
+ utm_campaign?: string;
101
+ utm_term?: string;
102
+ utm_content?: string;
103
+ /**
104
+ * Persistent visitor UUID (stored in localStorage).
105
+ * The server hashes this before storing — the raw value is never persisted.
106
+ */
107
+ visitor_id: string;
108
+ /**
109
+ * Per-session UUID (stored in sessionStorage).
110
+ * The server hashes this before storing — the raw value is never persisted.
111
+ */
112
+ session_id: string;
113
+ /** Event type — defaults to "pageview". Use for custom web events. */
114
+ event_name?: string;
115
+ /** ISO-8601 timestamp when the event occurred. */
116
+ occurred_at: string;
117
+ }
70
118
 
71
119
  declare class Watchup {
72
120
  private readonly cfg;
73
121
  private readonly batcher;
74
122
  private readonly cleanup;
123
+ private _user;
75
124
  /**
76
125
  * A random UUID generated on init. Stable for the lifetime of the page —
77
126
  * useful for correlating all events from one user session.
78
127
  */
79
128
  readonly sessionId: string;
129
+ /**
130
+ * Persistent visitor ID. Stored in localStorage so it survives browser
131
+ * sessions. Falls back to a per-session UUID when localStorage is blocked.
132
+ * The server hashes this value with SHA-256 before persisting.
133
+ */
134
+ private readonly visitorId;
135
+ /**
136
+ * Per-session ID stored in sessionStorage. Resets on tab close.
137
+ * The server hashes this value before persisting.
138
+ */
139
+ private readonly webSessionId;
80
140
  constructor(options: WatchupOptions);
141
+ private _getOrCreateVisitorId;
142
+ private _getOrCreateSessionId;
143
+ /**
144
+ * Attach a user to all subsequent errors, traces, and events.
145
+ * Call this after login; the context persists until `clearUser()` or page reload.
146
+ *
147
+ * @example
148
+ * watchup.setUser({ id: '42', email: 'ada@example.com', name: 'Ada Lovelace' });
149
+ */
150
+ setUser(user: WatchupUser): void;
151
+ /**
152
+ * Remove the current user context (e.g. after logout).
153
+ */
154
+ clearUser(): void;
81
155
  /**
82
156
  * Track a custom analytics event.
83
157
  *
@@ -85,6 +159,16 @@ declare class Watchup {
85
159
  * watchup.track('button.clicked', { label: 'Sign Up', variant: 'A' });
86
160
  */
87
161
  track(name: string, properties?: Record<string, unknown>): void;
162
+ /**
163
+ * Track a web analytics page view (or custom web event).
164
+ * Enriches the payload with visitor context, UTM params, and device info.
165
+ *
166
+ * Normally called automatically. Call manually when you need custom event_name.
167
+ *
168
+ * @example
169
+ * watchup.trackWebView({ event_name: 'conversion', path: '/checkout/success' });
170
+ */
171
+ trackWebView(overrides?: Partial<WebAnalyticsPayload>): void;
88
172
  /**
89
173
  * Manually capture an error.
90
174
  *
@@ -110,12 +194,13 @@ declare class Watchup {
110
194
  status?: TracePayload['status'];
111
195
  meta?: Record<string, unknown>;
112
196
  }) => void;
113
- /** Immediately flush all queued items. */
197
+ /** Immediately flush all queued items (both telemetry and web analytics). */
114
198
  flush(): void;
115
199
  /** Stop the flush timer and release all listeners. */
116
200
  shutdown(): void;
117
201
  private _setupAutoCapture;
118
202
  private _setupPageViewTracking;
203
+ private _timezone;
119
204
  }
120
205
 
121
- export { type ErrorPayload, type EventPayload, type IngestBatch, type TracePayload, Watchup, type WatchupOptions };
206
+ export { type ErrorPayload, type EventPayload, type IngestBatch, type TracePayload, Watchup, type WatchupOptions, type WatchupUser };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,11 @@
1
+ interface WatchupUser {
2
+ /** Your app's internal user ID — the only required field. */
3
+ id: string | number;
4
+ email?: string;
5
+ name?: string;
6
+ /** Any extra key/value pairs you want attached to events. */
7
+ [key: string]: unknown;
8
+ }
1
9
  interface WatchupOptions {
2
10
  /**
3
11
  * Your project's **public** API key from the Watchup dashboard.
@@ -46,6 +54,7 @@ interface TracePayload {
46
54
  environment?: string;
47
55
  release?: string;
48
56
  meta?: Record<string, unknown>;
57
+ user?: WatchupUser;
49
58
  }
50
59
  interface ErrorPayload {
51
60
  message: string;
@@ -56,6 +65,7 @@ interface ErrorPayload {
56
65
  timestamp: string;
57
66
  environment?: string;
58
67
  release?: string;
68
+ user?: WatchupUser;
59
69
  }
60
70
  interface EventPayload {
61
71
  name: string;
@@ -67,17 +77,81 @@ interface IngestBatch {
67
77
  errors?: ErrorPayload[];
68
78
  events?: EventPayload[];
69
79
  }
80
+ interface WebAnalyticsPayload {
81
+ /** URL path, e.g. "/pricing". */
82
+ path: string;
83
+ /** Hostname of the tracked site, e.g. "example.com". */
84
+ hostname: string;
85
+ /** Full referrer URL (document.referrer). */
86
+ referrer?: string;
87
+ /** document.title at tracking time. */
88
+ title?: string;
89
+ /** Screen width in CSS pixels. */
90
+ screen_w?: number;
91
+ /** Screen height in CSS pixels. */
92
+ screen_h?: number;
93
+ /** navigator.language, e.g. "en-GB". */
94
+ lang?: string;
95
+ /** Intl.DateTimeFormat().resolvedOptions().timeZone */
96
+ timezone?: string;
97
+ /** UTM parameters parsed from the current URL's query string. */
98
+ utm_source?: string;
99
+ utm_medium?: string;
100
+ utm_campaign?: string;
101
+ utm_term?: string;
102
+ utm_content?: string;
103
+ /**
104
+ * Persistent visitor UUID (stored in localStorage).
105
+ * The server hashes this before storing — the raw value is never persisted.
106
+ */
107
+ visitor_id: string;
108
+ /**
109
+ * Per-session UUID (stored in sessionStorage).
110
+ * The server hashes this before storing — the raw value is never persisted.
111
+ */
112
+ session_id: string;
113
+ /** Event type — defaults to "pageview". Use for custom web events. */
114
+ event_name?: string;
115
+ /** ISO-8601 timestamp when the event occurred. */
116
+ occurred_at: string;
117
+ }
70
118
 
71
119
  declare class Watchup {
72
120
  private readonly cfg;
73
121
  private readonly batcher;
74
122
  private readonly cleanup;
123
+ private _user;
75
124
  /**
76
125
  * A random UUID generated on init. Stable for the lifetime of the page —
77
126
  * useful for correlating all events from one user session.
78
127
  */
79
128
  readonly sessionId: string;
129
+ /**
130
+ * Persistent visitor ID. Stored in localStorage so it survives browser
131
+ * sessions. Falls back to a per-session UUID when localStorage is blocked.
132
+ * The server hashes this value with SHA-256 before persisting.
133
+ */
134
+ private readonly visitorId;
135
+ /**
136
+ * Per-session ID stored in sessionStorage. Resets on tab close.
137
+ * The server hashes this value before persisting.
138
+ */
139
+ private readonly webSessionId;
80
140
  constructor(options: WatchupOptions);
141
+ private _getOrCreateVisitorId;
142
+ private _getOrCreateSessionId;
143
+ /**
144
+ * Attach a user to all subsequent errors, traces, and events.
145
+ * Call this after login; the context persists until `clearUser()` or page reload.
146
+ *
147
+ * @example
148
+ * watchup.setUser({ id: '42', email: 'ada@example.com', name: 'Ada Lovelace' });
149
+ */
150
+ setUser(user: WatchupUser): void;
151
+ /**
152
+ * Remove the current user context (e.g. after logout).
153
+ */
154
+ clearUser(): void;
81
155
  /**
82
156
  * Track a custom analytics event.
83
157
  *
@@ -85,6 +159,16 @@ declare class Watchup {
85
159
  * watchup.track('button.clicked', { label: 'Sign Up', variant: 'A' });
86
160
  */
87
161
  track(name: string, properties?: Record<string, unknown>): void;
162
+ /**
163
+ * Track a web analytics page view (or custom web event).
164
+ * Enriches the payload with visitor context, UTM params, and device info.
165
+ *
166
+ * Normally called automatically. Call manually when you need custom event_name.
167
+ *
168
+ * @example
169
+ * watchup.trackWebView({ event_name: 'conversion', path: '/checkout/success' });
170
+ */
171
+ trackWebView(overrides?: Partial<WebAnalyticsPayload>): void;
88
172
  /**
89
173
  * Manually capture an error.
90
174
  *
@@ -110,12 +194,13 @@ declare class Watchup {
110
194
  status?: TracePayload['status'];
111
195
  meta?: Record<string, unknown>;
112
196
  }) => void;
113
- /** Immediately flush all queued items. */
197
+ /** Immediately flush all queued items (both telemetry and web analytics). */
114
198
  flush(): void;
115
199
  /** Stop the flush timer and release all listeners. */
116
200
  shutdown(): void;
117
201
  private _setupAutoCapture;
118
202
  private _setupPageViewTracking;
203
+ private _timezone;
119
204
  }
120
205
 
121
- export { type ErrorPayload, type EventPayload, type IngestBatch, type TracePayload, Watchup, type WatchupOptions };
206
+ export { type ErrorPayload, type EventPayload, type IngestBatch, type TracePayload, Watchup, type WatchupOptions, type WatchupUser };
package/dist/index.js CHANGED
@@ -3,7 +3,9 @@
3
3
  // src/transport.ts
4
4
  var Transport = class {
5
5
  constructor(baseUrl, apiKey, debug = false) {
6
- this.url = `${baseUrl.replace(/\/$/, "")}/api/v1/ingest/batch`;
6
+ const base = baseUrl.replace(/\/$/, "");
7
+ this.url = `${base}/api/v1/ingest/batch`;
8
+ this.webUrl = `${base}/api/v1/ingest/web-batch`;
7
9
  this.headers = {
8
10
  "Content-Type": "application/json",
9
11
  "X-Api-Key": apiKey
@@ -37,6 +39,31 @@ var Transport = class {
37
39
  if (this.debug) console.warn("[watchup] send failed:", err);
38
40
  }
39
41
  }
42
+ /**
43
+ * Send web analytics batch to the dedicated /web-batch endpoint.
44
+ * Never rejects.
45
+ */
46
+ async sendWeb(batch) {
47
+ try {
48
+ const body = JSON.stringify(batch);
49
+ if (body.length > 6e4) {
50
+ this.beaconWeb(batch);
51
+ return;
52
+ }
53
+ const res = await fetch(this.webUrl, {
54
+ method: "POST",
55
+ headers: this.headers,
56
+ body,
57
+ keepalive: true
58
+ });
59
+ if (this.debug && !res.ok) {
60
+ const text = await res.text().catch(() => "");
61
+ console.warn(`[watchup] web-batch ${res.status}: ${text}`);
62
+ }
63
+ } catch (err) {
64
+ if (this.debug) console.warn("[watchup] sendWeb failed:", err);
65
+ }
66
+ }
40
67
  /**
41
68
  * Send via `navigator.sendBeacon`.
42
69
  * Returns `true` if the browser accepted the request (doesn't guarantee delivery).
@@ -51,6 +78,18 @@ var Transport = class {
51
78
  return false;
52
79
  }
53
80
  }
81
+ /**
82
+ * sendBeacon variant for web analytics events.
83
+ */
84
+ beaconWeb(batch) {
85
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return false;
86
+ try {
87
+ const blob = new Blob([JSON.stringify(batch)], { type: "application/json" });
88
+ return navigator.sendBeacon(this.webUrl, blob);
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
54
93
  };
55
94
 
56
95
  // src/batcher.ts
@@ -59,6 +98,7 @@ var Batcher = class {
59
98
  this.traces = [];
60
99
  this.errors = [];
61
100
  this.events = [];
101
+ this.webViews = [];
62
102
  this.timer = null;
63
103
  this.flushing = false;
64
104
  this.transport = transport;
@@ -79,6 +119,7 @@ var Batcher = class {
79
119
  this.timer = null;
80
120
  }
81
121
  }
122
+ // ── Telemetry queue ───────────────────────────────────────────────────────
82
123
  addTrace(t) {
83
124
  this.traces.push(t);
84
125
  if (this.traces.length >= this.maxBatchSize) this.flush();
@@ -91,27 +132,49 @@ var Batcher = class {
91
132
  this.events.push(e);
92
133
  if (this.events.length >= this.maxBatchSize) this.flush();
93
134
  }
94
- drain() {
135
+ // ── Web analytics queue ───────────────────────────────────────────────────
136
+ addWebView(payload) {
137
+ this.webViews.push(payload);
138
+ if (this.webViews.length >= this.maxBatchSize) this.flushWeb();
139
+ }
140
+ // ── Drain helpers ─────────────────────────────────────────────────────────
141
+ drainTelemetry() {
95
142
  const traces = this.traces.splice(0);
96
143
  const errors = this.errors.splice(0);
97
144
  const events = this.events.splice(0);
98
145
  if (!traces.length && !errors.length && !events.length) return null;
99
146
  return { traces, errors, events };
100
147
  }
148
+ drainWeb() {
149
+ const web = this.webViews.splice(0);
150
+ if (!web.length) return null;
151
+ return { web };
152
+ }
153
+ // ── Flush ─────────────────────────────────────────────────────────────────
101
154
  flush() {
102
- if (this.flushing) return;
103
- const batch = this.drain();
104
- if (!batch) return;
105
- this.flushing = true;
106
- this.transport.send(batch).finally(() => {
107
- this.flushing = false;
108
- });
155
+ if (!this.flushing) {
156
+ const batch = this.drainTelemetry();
157
+ if (batch) {
158
+ this.flushing = true;
159
+ this.transport.send(batch).finally(() => {
160
+ this.flushing = false;
161
+ });
162
+ }
163
+ }
164
+ this.flushWeb();
165
+ }
166
+ flushWeb() {
167
+ const batch = this.drainWeb();
168
+ if (batch) this.transport.sendWeb(batch);
109
169
  }
110
170
  beaconFlush() {
111
- const batch = this.drain();
112
- if (!batch) return;
113
- if (!this.transport.beacon(batch)) {
114
- this.transport.send(batch);
171
+ const batch = this.drainTelemetry();
172
+ if (batch) {
173
+ if (!this.transport.beacon(batch)) this.transport.send(batch);
174
+ }
175
+ const webBatch = this.drainWeb();
176
+ if (webBatch) {
177
+ if (!this.transport.beaconWeb(webBatch)) this.transport.sendWeb(webBatch);
115
178
  }
116
179
  }
117
180
  };
@@ -267,9 +330,12 @@ var DEFAULTS = {
267
330
  pageViews: true
268
331
  }
269
332
  };
333
+ var VISITOR_KEY = "__wup_vid";
334
+ var SESSION_KEY = "__wup_sid";
270
335
  var Watchup = class {
271
336
  constructor(options) {
272
337
  this.cleanup = [];
338
+ this._user = null;
273
339
  /**
274
340
  * A random UUID generated on init. Stable for the lifetime of the page —
275
341
  * useful for correlating all events from one user session.
@@ -286,8 +352,52 @@ var Watchup = class {
286
352
  const transport = new Transport(this.cfg.baseUrl, this.cfg.apiKey, this.cfg.debug);
287
353
  this.batcher = new Batcher(transport, this.cfg.flushInterval, this.cfg.maxBatchSize);
288
354
  this.batcher.start();
355
+ this.visitorId = this._getOrCreateVisitorId();
356
+ this.webSessionId = this._getOrCreateSessionId();
289
357
  this._setupAutoCapture();
290
358
  }
359
+ // ── Visitor / session identity helpers ─────────────────────────────────────
360
+ _getOrCreateVisitorId() {
361
+ try {
362
+ let id = localStorage.getItem(VISITOR_KEY);
363
+ if (!id) {
364
+ id = crypto.randomUUID();
365
+ localStorage.setItem(VISITOR_KEY, id);
366
+ }
367
+ return id;
368
+ } catch {
369
+ return crypto.randomUUID();
370
+ }
371
+ }
372
+ _getOrCreateSessionId() {
373
+ try {
374
+ let id = sessionStorage.getItem(SESSION_KEY);
375
+ if (!id) {
376
+ id = crypto.randomUUID();
377
+ sessionStorage.setItem(SESSION_KEY, id);
378
+ }
379
+ return id;
380
+ } catch {
381
+ return this.sessionId;
382
+ }
383
+ }
384
+ // ── User identification ───────────────────────────────────────────────────
385
+ /**
386
+ * Attach a user to all subsequent errors, traces, and events.
387
+ * Call this after login; the context persists until `clearUser()` or page reload.
388
+ *
389
+ * @example
390
+ * watchup.setUser({ id: '42', email: 'ada@example.com', name: 'Ada Lovelace' });
391
+ */
392
+ setUser(user) {
393
+ this._user = { ...user };
394
+ }
395
+ /**
396
+ * Remove the current user context (e.g. after logout).
397
+ */
398
+ clearUser() {
399
+ this._user = null;
400
+ }
291
401
  // ── Public API ────────────────────────────────────────────────────────────
292
402
  /**
293
403
  * Track a custom analytics event.
@@ -304,6 +414,44 @@ var Watchup = class {
304
414
  };
305
415
  this.batcher.addEvent(event);
306
416
  }
417
+ /**
418
+ * Track a web analytics page view (or custom web event).
419
+ * Enriches the payload with visitor context, UTM params, and device info.
420
+ *
421
+ * Normally called automatically. Call manually when you need custom event_name.
422
+ *
423
+ * @example
424
+ * watchup.trackWebView({ event_name: 'conversion', path: '/checkout/success' });
425
+ */
426
+ trackWebView(overrides = {}) {
427
+ var _a, _b;
428
+ const url = new URL(window.location.href);
429
+ const params = url.searchParams;
430
+ const payload = {
431
+ path: url.pathname + (url.search || ""),
432
+ hostname: url.hostname,
433
+ referrer: document.referrer || void 0,
434
+ title: document.title || void 0,
435
+ screen_w: (_a = window.screen) == null ? void 0 : _a.width,
436
+ screen_h: (_b = window.screen) == null ? void 0 : _b.height,
437
+ lang: navigator.language || void 0,
438
+ timezone: this._timezone(),
439
+ // UTM parameters
440
+ utm_source: params.get("utm_source") || void 0,
441
+ utm_medium: params.get("utm_medium") || void 0,
442
+ utm_campaign: params.get("utm_campaign") || void 0,
443
+ utm_term: params.get("utm_term") || void 0,
444
+ utm_content: params.get("utm_content") || void 0,
445
+ // Identity (raw; the server hashes before storing)
446
+ visitor_id: this.visitorId,
447
+ session_id: this.webSessionId,
448
+ event_name: "pageview",
449
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
450
+ // Apply caller overrides last
451
+ ...overrides
452
+ };
453
+ this.batcher.addWebView(payload);
454
+ }
307
455
  /**
308
456
  * Manually capture an error.
309
457
  *
@@ -325,7 +473,8 @@ var Watchup = class {
325
473
  },
326
474
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
327
475
  environment: this.cfg.environment,
328
- ...this.cfg.release && { release: this.cfg.release }
476
+ ...this.cfg.release && { release: this.cfg.release },
477
+ ...this._user && { user: this._user }
329
478
  };
330
479
  this.batcher.addError(payload);
331
480
  }
@@ -351,11 +500,12 @@ var Watchup = class {
351
500
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
352
501
  environment: this.cfg.environment,
353
502
  ...this.cfg.release && { release: this.cfg.release },
354
- ...opts.meta && { meta: opts.meta }
503
+ ...opts.meta && { meta: opts.meta },
504
+ ...this._user && { user: this._user }
355
505
  });
356
506
  };
357
507
  }
358
- /** Immediately flush all queued items. */
508
+ /** Immediately flush all queued items (both telemetry and web analytics). */
359
509
  flush() {
360
510
  this.batcher.flush();
361
511
  }
@@ -383,33 +533,24 @@ var Watchup = class {
383
533
  }
384
534
  }
385
535
  _setupPageViewTracking() {
386
- const track = () => {
387
- this.batcher.addEvent({
388
- name: "pageview",
389
- properties: {
390
- path: window.location.pathname,
391
- ...window.location.search && { search: window.location.search },
392
- ...document.referrer && { referrer: document.referrer },
393
- title: document.title
394
- },
395
- occurred_at: (/* @__PURE__ */ new Date()).toISOString()
396
- });
536
+ const trackView = () => {
537
+ setTimeout(() => this.trackWebView(), 0);
397
538
  };
398
539
  if (document.readyState === "loading") {
399
- document.addEventListener("DOMContentLoaded", track, { once: true });
540
+ document.addEventListener("DOMContentLoaded", trackView, { once: true });
400
541
  } else {
401
- setTimeout(track, 0);
542
+ setTimeout(() => this.trackWebView(), 0);
402
543
  }
403
544
  const origPush = history.pushState.bind(history);
404
545
  const origReplace = history.replaceState.bind(history);
405
546
  history.pushState = (...args) => {
406
547
  origPush(...args);
407
- setTimeout(track, 0);
548
+ trackView();
408
549
  };
409
550
  history.replaceState = (...args) => {
410
551
  origReplace(...args);
411
552
  };
412
- const onPopState = () => setTimeout(track, 0);
553
+ const onPopState = () => trackView();
413
554
  window.addEventListener("popstate", onPopState);
414
555
  this.cleanup.push(() => {
415
556
  history.pushState = origPush;
@@ -417,6 +558,14 @@ var Watchup = class {
417
558
  window.removeEventListener("popstate", onPopState);
418
559
  });
419
560
  }
561
+ // ── Helpers ───────────────────────────────────────────────────────────────
562
+ _timezone() {
563
+ try {
564
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || void 0;
565
+ } catch {
566
+ return void 0;
567
+ }
568
+ }
420
569
  };
421
570
 
422
571
  exports.Watchup = Watchup;