@ziplogger/browser 0.3.3 → 0.4.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/README.md +29 -3
- package/index.d.ts +40 -4
- package/index.js +162 -6
- package/package.json +8 -5
- package/react.js +3 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @ziplogger/browser
|
|
2
2
|
|
|
3
|
-
Browser SDK for [ZipLogger](https://ziplogger.dev) —
|
|
4
|
-
unhandled promise rejections
|
|
5
|
-
boundary. Zero dependencies.
|
|
3
|
+
Browser SDK for [ZipLogger](https://ziplogger.dev) — product-analytics events,
|
|
4
|
+
uncaught errors, unhandled promise rejections and distributed tracing from web apps, with a
|
|
5
|
+
first-class React error boundary. Zero dependencies.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install @ziplogger/browser
|
|
@@ -25,6 +25,32 @@ ziplogger.log({ severity: 'info', message: 'checkout started', fields: { cartVal
|
|
|
25
25
|
try { risky() } catch (err) { ziplogger.captureError(err, { step: 'payment' }) }
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## Events
|
|
29
|
+
|
|
30
|
+
Events are what people did; logs are what you read when something breaks. They are separate calls
|
|
31
|
+
because ZipLogger answers different questions with each.
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
ziplogger.track('checkout_started', { cartValue: 214.9, currency: 'USD' })
|
|
35
|
+
|
|
36
|
+
// After sign-in. Links everything this browser did anonymously to the account, so the visitor
|
|
37
|
+
// stops being counted as two people.
|
|
38
|
+
ziplogger.identify('user_42')
|
|
39
|
+
|
|
40
|
+
ziplogger.reset() // on sign-out: a fresh anonymous identity from here
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
An un-identified visitor still needs an id, or the server has nobody to attribute the event to. The
|
|
44
|
+
SDK mints one on first use and keeps it in `localStorage` (`zl_anon`), with a per-tab session id in
|
|
45
|
+
`sessionStorage` (`zl_sess`) — that stored anonymous id is exactly what `identify()` later links.
|
|
46
|
+
Both fall back to memory when storage is unavailable, so private mode degrades to per-page
|
|
47
|
+
attribution rather than an error.
|
|
48
|
+
|
|
49
|
+
Every event carries an `insertId`, so a retry after a timeout cannot count it twice.
|
|
50
|
+
|
|
51
|
+
**Do not put credentials in properties.** Values that look like tokens, keys or card numbers are
|
|
52
|
+
redacted server-side, but the safe habit is not to send them.
|
|
53
|
+
|
|
28
54
|
Every event carries `url`, `userAgent`, and `environment` fields automatically; `Error` objects
|
|
29
55
|
map to ZipLogger's `stackTrace`. A final `fetch(…, { keepalive: true })` flush fires on
|
|
30
56
|
`pagehide` so events survive navigation and tab closes.
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export type Severity = 'debug' | 'info' | 'warn' | 'error' | 'fatal'
|
|
2
2
|
|
|
3
3
|
export interface BrowserOptions {
|
|
4
|
-
/**
|
|
4
|
+
/** Your ZipLogger origin, e.g. "https://app.ziplogger.dev" (or your own host if
|
|
5
|
+
* you self-host). Paths are appended for you. */
|
|
5
6
|
endpoint: string
|
|
6
7
|
/** Tenant ingestion API key (zk_...). Use a key dedicated to browser traffic. */
|
|
7
8
|
apiKey: string
|
|
@@ -12,8 +13,15 @@ export interface BrowserOptions {
|
|
|
12
13
|
/** Default "production". */
|
|
13
14
|
environment?: string
|
|
14
15
|
tags?: string[]
|
|
15
|
-
/** Attach url + userAgent
|
|
16
|
+
/** Attach url + userAgent to every log line, and url + page to every event. Default true. */
|
|
16
17
|
includePageContext?: boolean
|
|
18
|
+
/** Your id for the signed-in user, when the page already knows it. Otherwise call identify(). */
|
|
19
|
+
userId?: string
|
|
20
|
+
/** Override the generated anonymous id. Normally left alone: it is minted once and kept in
|
|
21
|
+
* localStorage so a visitor's pre-login events can be linked to their account later. */
|
|
22
|
+
anonymousId?: string
|
|
23
|
+
/** Override the generated session id (per tab, kept in sessionStorage). */
|
|
24
|
+
sessionId?: string
|
|
17
25
|
/** Max buffered events. Default 1000. */
|
|
18
26
|
queueCapacity?: number
|
|
19
27
|
/** Max events per request. Default 20. */
|
|
@@ -38,16 +46,42 @@ export interface BrowserLogEntry {
|
|
|
38
46
|
tags?: string[]
|
|
39
47
|
}
|
|
40
48
|
|
|
49
|
+
export interface Identity {
|
|
50
|
+
userId: string | null
|
|
51
|
+
anonymousId: string | null
|
|
52
|
+
sessionId: string | null
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
export declare class ZipLoggerBrowser {
|
|
42
56
|
constructor(options: BrowserOptions)
|
|
43
|
-
/**
|
|
57
|
+
/** Records lost to backpressure or exhausted retries, logs and events together. */
|
|
44
58
|
dropped: number
|
|
59
|
+
/** The ids events are currently attributed to. */
|
|
60
|
+
readonly identity: Identity
|
|
45
61
|
/** Queue an event for background delivery. Never blocks, never throws. */
|
|
46
62
|
log(entry: BrowserLogEntry): void
|
|
47
63
|
/** Report a caught error with optional context fields. */
|
|
48
64
|
captureError(error: unknown, fields?: Record<string, unknown>): void
|
|
49
65
|
/** Capture window error / unhandledrejection events. Returns a stop function. */
|
|
50
66
|
captureGlobalErrors(): () => void
|
|
67
|
+
/**
|
|
68
|
+
* Record a product-analytics event -- a signup, a checkout, a plan change.
|
|
69
|
+
*
|
|
70
|
+
* Distinct from log(): logs are lines you read when something breaks, events are things people
|
|
71
|
+
* did, and ZipLogger answers different questions with each. Never blocks, never throws.
|
|
72
|
+
*
|
|
73
|
+
* Values that look like credentials are redacted server-side; do not send passwords, tokens or
|
|
74
|
+
* card numbers as properties.
|
|
75
|
+
*/
|
|
76
|
+
track(name: string, properties?: Record<string, unknown>): void
|
|
77
|
+
/**
|
|
78
|
+
* Attach this browser's anonymous history to a real account and use that id from now on.
|
|
79
|
+
* Call once after sign-in; the server links the ids so pre-login events stop being a separate
|
|
80
|
+
* person.
|
|
81
|
+
*/
|
|
82
|
+
identify(userId: string, properties?: Record<string, unknown>): void
|
|
83
|
+
/** Forget the signed-in user and start a fresh anonymous identity, e.g. on sign-out. */
|
|
84
|
+
reset(): void
|
|
51
85
|
/**
|
|
52
86
|
* Wraps window.fetch: adds a W3C traceparent header to same-origin requests (plus any
|
|
53
87
|
* origins in propagateTo) so browser calls and backend traces share one trace id, and
|
|
@@ -61,7 +95,7 @@ export declare class ZipLoggerBrowser {
|
|
|
61
95
|
/** Service name for browser spans (default "<source>-browser"). */
|
|
62
96
|
serviceName?: string
|
|
63
97
|
}): () => void
|
|
64
|
-
/** Send anything still buffered. keepalive=true during page unload. */
|
|
98
|
+
/** Send anything still buffered, logs and events both. keepalive=true during page unload. */
|
|
65
99
|
flush(keepalive?: boolean): Promise<void>
|
|
66
100
|
/** Flush and detach global listeners. */
|
|
67
101
|
close(): Promise<void>
|
|
@@ -85,4 +119,6 @@ export declare function createUseZipLogger(
|
|
|
85
119
|
): () => {
|
|
86
120
|
captureError: (error: unknown, fields?: Record<string, unknown>) => void
|
|
87
121
|
log: (entry: BrowserLogEntry) => void
|
|
122
|
+
track: (name: string, properties?: Record<string, unknown>) => void
|
|
123
|
+
identify: (userId: string, properties?: Record<string, unknown>) => void
|
|
88
124
|
}
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ZipLogger browser SDK.
|
|
2
|
+
* ZipLogger browser SDK — logs, errors, traces and product-analytics events.
|
|
3
3
|
*
|
|
4
4
|
* Same delivery semantics as every ZipLogger SDK — bounded queue, NDJSON batches,
|
|
5
5
|
* retry with backoff honoring 429 Retry-After, drop-on-backpressure, never throws —
|
|
@@ -29,6 +29,9 @@ export class ZipLoggerBrowser {
|
|
|
29
29
|
|
|
30
30
|
const trimmed = String(options.endpoint).replace(/\/+$/, '')
|
|
31
31
|
this._url = /\/logs$/i.test(trimmed) ? trimmed : trimmed + '/ingest/v1/logs'
|
|
32
|
+
// Events are a different endpoint with a different payload, so they get their own URL and
|
|
33
|
+
// queue rather than being squeezed through the log pipeline.
|
|
34
|
+
this._eventsUrl = trimmed.replace(/\/ingest\/v1\/logs$/i, '') + '/ingest/v1/events'
|
|
32
35
|
this._apiKey = options.apiKey
|
|
33
36
|
this._source = options.source || (HAS_WINDOW ? window.location.hostname : 'browser')
|
|
34
37
|
this._release = options.release
|
|
@@ -49,6 +52,18 @@ export class ZipLoggerBrowser {
|
|
|
49
52
|
this._sending = Promise.resolve()
|
|
50
53
|
this._detach = []
|
|
51
54
|
|
|
55
|
+
this._events = []
|
|
56
|
+
this._eventTimer = null
|
|
57
|
+
this._eventSending = Promise.resolve()
|
|
58
|
+
|
|
59
|
+
// An event with neither a user id nor an anonymous id is rejected by the server, so a
|
|
60
|
+
// visitor who has not signed in needs a stable id of their own. It lives in localStorage so
|
|
61
|
+
// it survives reloads -- that is what lets identify() later attach their pre-login events to
|
|
62
|
+
// the account. Session id is per-tab and per-visit, so it belongs in sessionStorage.
|
|
63
|
+
this._userId = options.userId ?? null
|
|
64
|
+
this._anonymousId = options.anonymousId ?? this._persistedId('local', 'zl_anon', 'anon')
|
|
65
|
+
this._sessionId = options.sessionId ?? this._persistedId('session', 'zl_sess', 'sess')
|
|
66
|
+
|
|
52
67
|
if (HAS_WINDOW) {
|
|
53
68
|
const onHide = () => { void this.flush(true) }
|
|
54
69
|
window.addEventListener('pagehide', onHide)
|
|
@@ -103,6 +118,91 @@ export class ZipLoggerBrowser {
|
|
|
103
118
|
})
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Record a product-analytics event: a signup, a checkout, a plan change.
|
|
123
|
+
*
|
|
124
|
+
* Separate from `log()` on purpose. Logs are lines you read when something breaks; events are
|
|
125
|
+
* things people did, and they answer different questions in different places in ZipLogger.
|
|
126
|
+
* Never blocks, never throws.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} name Event name, e.g. "checkout_started". Lower-cased server-side.
|
|
129
|
+
* @param {Record<string, unknown>} [properties] Your own properties. Values that look like
|
|
130
|
+
* credentials are redacted server-side; do not send passwords, tokens or card numbers.
|
|
131
|
+
*/
|
|
132
|
+
track(name, properties) {
|
|
133
|
+
if (!name || typeof name !== 'string') return
|
|
134
|
+
if (this._events.length >= this._queueCapacity) { this.dropped++; return }
|
|
135
|
+
|
|
136
|
+
const event = {
|
|
137
|
+
type: 'track',
|
|
138
|
+
name,
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
userId: this._userId ?? undefined,
|
|
141
|
+
anonymousId: this._anonymousId ?? undefined,
|
|
142
|
+
sessionId: this._sessionId ?? undefined,
|
|
143
|
+
environment: this._environment,
|
|
144
|
+
release: this._release,
|
|
145
|
+
commitSha: this._commitSha,
|
|
146
|
+
// An idempotency key, so a retry after a timeout cannot count the same event twice.
|
|
147
|
+
insertId: randomHex(12),
|
|
148
|
+
properties: properties && typeof properties === 'object' ? properties : undefined,
|
|
149
|
+
}
|
|
150
|
+
if (this._includePageContext && HAS_WINDOW) {
|
|
151
|
+
event.url = window.location.href
|
|
152
|
+
event.page = window.location.pathname
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this._events.push(event)
|
|
156
|
+
this._scheduleEvents(this._events.length >= this._batchSize ? 0 : this._flushInterval)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Attach everything this browser has done anonymously to a real account, and use the account
|
|
161
|
+
* id from now on.
|
|
162
|
+
*
|
|
163
|
+
* Call it once after sign-in. The server links the two ids, so the events this visitor sent
|
|
164
|
+
* before signing in stop being a separate person -- which is the whole point of holding an
|
|
165
|
+
* anonymous id in the first place.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} userId Your own id for the user.
|
|
168
|
+
* @param {Record<string, unknown>} [properties] Optional properties for the identify event.
|
|
169
|
+
*/
|
|
170
|
+
identify(userId, properties) {
|
|
171
|
+
if (!userId || typeof userId !== 'string') return
|
|
172
|
+
const anonymousId = this._anonymousId
|
|
173
|
+
|
|
174
|
+
this._userId = userId
|
|
175
|
+
if (!anonymousId) return // nothing to link; later events simply carry the user id
|
|
176
|
+
|
|
177
|
+
if (this._events.length >= this._queueCapacity) { this.dropped++; return }
|
|
178
|
+
this._events.push({
|
|
179
|
+
type: 'identify',
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
userId,
|
|
182
|
+
anonymousId,
|
|
183
|
+
sessionId: this._sessionId ?? undefined,
|
|
184
|
+
environment: this._environment,
|
|
185
|
+
insertId: randomHex(12),
|
|
186
|
+
properties: properties && typeof properties === 'object' ? properties : undefined,
|
|
187
|
+
})
|
|
188
|
+
// Linking is what every later event depends on, so it does not wait for a full batch.
|
|
189
|
+
this._scheduleEvents(0)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Forget the signed-in user, e.g. on sign-out. Later events are anonymous again. */
|
|
193
|
+
reset() {
|
|
194
|
+
this._userId = null
|
|
195
|
+
this._anonymousId = this._newId('anon')
|
|
196
|
+
this._sessionId = this._newId('sess')
|
|
197
|
+
this._store('local', 'zl_anon', this._anonymousId)
|
|
198
|
+
this._store('session', 'zl_sess', this._sessionId)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The ids this client is currently attributing events to. Useful in tests and debugging. */
|
|
202
|
+
get identity() {
|
|
203
|
+
return { userId: this._userId, anonymousId: this._anonymousId, sessionId: this._sessionId }
|
|
204
|
+
}
|
|
205
|
+
|
|
106
206
|
/**
|
|
107
207
|
* Start capturing window `error` and `unhandledrejection` events.
|
|
108
208
|
* Returns a function that stops capturing.
|
|
@@ -257,6 +357,51 @@ export class ZipLoggerBrowser {
|
|
|
257
357
|
return stop
|
|
258
358
|
}
|
|
259
359
|
|
|
360
|
+
/** A stable id from web storage, minted on first use. Falls back to memory when storage is
|
|
361
|
+
* unavailable (private mode, blocked cookies) -- attribution is then per-page, never an error. */
|
|
362
|
+
_persistedId(kind, key, prefix) {
|
|
363
|
+
const existing = this._read(kind, key)
|
|
364
|
+
if (existing) return existing
|
|
365
|
+
const id = this._newId(prefix)
|
|
366
|
+
this._store(kind, key, id)
|
|
367
|
+
return id
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_newId(prefix) { return `${prefix}_${randomHex(10)}` }
|
|
371
|
+
|
|
372
|
+
_read(kind, key) {
|
|
373
|
+
try {
|
|
374
|
+
const store = kind === 'local' ? globalThis.localStorage : globalThis.sessionStorage
|
|
375
|
+
return store ? store.getItem(key) : null
|
|
376
|
+
} catch { return null } // storage disabled: not an error worth surfacing
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
_store(kind, key, value) {
|
|
380
|
+
try {
|
|
381
|
+
const store = kind === 'local' ? globalThis.localStorage : globalThis.sessionStorage
|
|
382
|
+
if (store) store.setItem(key, value)
|
|
383
|
+
} catch { /* storage disabled; the id stays in memory for this page */ }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_scheduleEvents(delay) {
|
|
387
|
+
if (this._eventTimer !== null) {
|
|
388
|
+
if (delay > 0) return
|
|
389
|
+
clearTimeout(this._eventTimer)
|
|
390
|
+
}
|
|
391
|
+
this._eventTimer = setTimeout(() => {
|
|
392
|
+
this._eventTimer = null
|
|
393
|
+
this._eventSending = this._eventSending.then(() => this._drainEvents(false)).catch(() => {})
|
|
394
|
+
}, delay)
|
|
395
|
+
if (typeof this._eventTimer === 'object' && this._eventTimer.unref) this._eventTimer.unref()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async _drainEvents(keepalive) {
|
|
399
|
+
while (this._events.length > 0) {
|
|
400
|
+
const batch = this._events.splice(0, this._batchSize)
|
|
401
|
+
await this._post(this._eventsUrl, batch, batch.length, keepalive)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
260
405
|
_schedule(delay) {
|
|
261
406
|
if (this._timer !== null) {
|
|
262
407
|
if (delay > 0) return
|
|
@@ -277,12 +422,21 @@ export class ZipLoggerBrowser {
|
|
|
277
422
|
}
|
|
278
423
|
|
|
279
424
|
async _send(batch, keepalive) {
|
|
425
|
+
await this._post(this._url, batch, batch.length, keepalive)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* One NDJSON POST with the retry policy both queues share: retry only what retrying can fix
|
|
430
|
+
* (429, 408, 5xx), honour Retry-After, and never retry during page unload -- an unloading page
|
|
431
|
+
* has no time, and a duplicate is worse than a loss when the event already carries an insertId.
|
|
432
|
+
*/
|
|
433
|
+
async _post(url, batch, count, keepalive) {
|
|
280
434
|
const payload = batch.map((e) => JSON.stringify(e)).join('\n')
|
|
281
435
|
|
|
282
436
|
for (let attempt = 0; ; attempt++) {
|
|
283
437
|
let retryAfterMs = null
|
|
284
438
|
try {
|
|
285
|
-
const response = await fetch(
|
|
439
|
+
const response = await fetch(url, {
|
|
286
440
|
method: 'POST',
|
|
287
441
|
headers: { 'Content-Type': 'application/x-ndjson', 'X-Api-Key': this._apiKey },
|
|
288
442
|
body: payload,
|
|
@@ -290,7 +444,7 @@ export class ZipLoggerBrowser {
|
|
|
290
444
|
})
|
|
291
445
|
if (response.ok) return
|
|
292
446
|
if (response.status !== 429 && response.status !== 408 && response.status < 500) {
|
|
293
|
-
this.dropped +=
|
|
447
|
+
this.dropped += count
|
|
294
448
|
return
|
|
295
449
|
}
|
|
296
450
|
const header = response.headers.get('retry-after')
|
|
@@ -300,7 +454,7 @@ export class ZipLoggerBrowser {
|
|
|
300
454
|
}
|
|
301
455
|
|
|
302
456
|
if (keepalive || attempt >= this._maxRetries) {
|
|
303
|
-
this.dropped +=
|
|
457
|
+
this.dropped += count // unloading pages don't get retries
|
|
304
458
|
return
|
|
305
459
|
}
|
|
306
460
|
await new Promise((resolve) => {
|
|
@@ -310,11 +464,13 @@ export class ZipLoggerBrowser {
|
|
|
310
464
|
}
|
|
311
465
|
}
|
|
312
466
|
|
|
313
|
-
/** Send anything still buffered. Pass keepalive=true during page unload. */
|
|
467
|
+
/** Send anything still buffered, logs and events both. Pass keepalive=true during page unload. */
|
|
314
468
|
async flush(keepalive = false) {
|
|
315
469
|
if (this._timer !== null) { clearTimeout(this._timer); this._timer = null }
|
|
470
|
+
if (this._eventTimer !== null) { clearTimeout(this._eventTimer); this._eventTimer = null }
|
|
316
471
|
this._sending = this._sending.then(() => this._drain(keepalive)).catch(() => {})
|
|
317
|
-
|
|
472
|
+
this._eventSending = this._eventSending.then(() => this._drainEvents(keepalive)).catch(() => {})
|
|
473
|
+
await Promise.all([this._sending, this._eventSending])
|
|
318
474
|
}
|
|
319
475
|
|
|
320
476
|
/** Flush and detach all global listeners. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziplogger/browser",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ZipLogger browser SDK \u2014
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "ZipLogger browser SDK \u2014 product-analytics events, console errors, unhandled rejections and distributed tracing from web apps, with a React error boundary. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
@@ -22,19 +22,22 @@
|
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"ziplogger",
|
|
25
|
+
"analytics",
|
|
26
|
+
"events",
|
|
25
27
|
"logging",
|
|
26
28
|
"browser",
|
|
27
29
|
"react",
|
|
28
|
-
"error-tracking"
|
|
30
|
+
"error-tracking",
|
|
31
|
+
"opentelemetry"
|
|
29
32
|
],
|
|
30
33
|
"repository": {
|
|
31
34
|
"type": "git",
|
|
32
|
-
"url": "git+https://github.com/
|
|
35
|
+
"url": "git+https://github.com/ziploggerhq/ZipLogger_Client.git",
|
|
33
36
|
"directory": "sdk_browser"
|
|
34
37
|
},
|
|
35
38
|
"homepage": "https://ziplogger.dev",
|
|
36
39
|
"bugs": {
|
|
37
|
-
"url": "https://github.com/
|
|
40
|
+
"url": "https://github.com/ziploggerhq/ZipLogger_Client/issues"
|
|
38
41
|
},
|
|
39
42
|
"author": "Ahaliav Fox",
|
|
40
43
|
"files": [
|
package/react.js
CHANGED
|
@@ -50,12 +50,14 @@ export function createErrorBoundary(React, client) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/** Convenience hook factory:
|
|
53
|
+
/** Convenience hook factory: stable callbacks for logging, error capture and events. */
|
|
54
54
|
export function createUseZipLogger(React, client) {
|
|
55
55
|
return function useZipLogger() {
|
|
56
56
|
return React.useMemo(() => ({
|
|
57
57
|
captureError: (error, fields) => client.captureError(error, fields),
|
|
58
58
|
log: (entry) => client.log(entry),
|
|
59
|
+
track: (name, properties) => client.track(name, properties),
|
|
60
|
+
identify: (userId, properties) => client.identify(userId, properties),
|
|
59
61
|
}), [])
|
|
60
62
|
}
|
|
61
63
|
}
|