@redthreadlabs/tracelog-client 1.7.0 → 2.0.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.
@@ -1,21 +1,19 @@
1
- import { JsonValue, LogEventItem } from './types';
2
- export type EventEnqueuer = (event: LogEventItem) => void;
1
+ import { JsonValue, EventRecord } from './types';
2
+ export type EventEnqueuer = (event: EventRecord) => void;
3
3
  export declare class EventBuilder {
4
4
  private _enqueue;
5
5
  private _type;
6
6
  private _level;
7
7
  private _message;
8
- private _duration?;
9
8
  private _error?;
10
- private _params?;
9
+ private _labels?;
11
10
  constructor(enqueue: EventEnqueuer, type: string);
12
11
  info(message: string): this;
13
12
  warn(message: string): this;
14
13
  error(message: string): this;
15
14
  debug(message: string): this;
16
- withParam(key: string, value: JsonValue): this;
17
- withParams(params: Record<string, JsonValue>): this;
15
+ withLabel(key: string, value: JsonValue): this;
16
+ withLabels(labels: Record<string, JsonValue>): this;
18
17
  withError(err: any): this;
19
- withDuration(ms: number): this;
20
18
  send(): void;
21
19
  }
@@ -29,16 +29,16 @@ class EventBuilder {
29
29
  this._message = message;
30
30
  return this;
31
31
  }
32
- withParam(key, value) {
33
- if (!this._params)
34
- this._params = {};
35
- this._params[key] = value;
32
+ withLabel(key, value) {
33
+ if (!this._labels)
34
+ this._labels = {};
35
+ this._labels[key] = value;
36
36
  return this;
37
37
  }
38
- withParams(params) {
39
- if (!this._params)
40
- this._params = {};
41
- Object.assign(this._params, params);
38
+ withLabels(labels) {
39
+ if (!this._labels)
40
+ this._labels = {};
41
+ Object.assign(this._labels, labels);
42
42
  return this;
43
43
  }
44
44
  withError(err) {
@@ -73,31 +73,24 @@ class EventBuilder {
73
73
  }
74
74
  return this;
75
75
  }
76
- withDuration(ms) {
77
- this._duration = ms;
78
- return this;
79
- }
80
76
  send() {
81
77
  const event = {
82
78
  type: this._type,
83
- timestamp: Date.now(),
79
+ timestamp: Date.now() * 1000, // epoch microseconds (the on-disk unit)
84
80
  level: this._level,
85
81
  message: this._message,
86
82
  tz_offset: (0, util_1.tzOffsetMinutes)(),
87
83
  };
88
- if (this._duration !== undefined)
89
- event.duration = this._duration;
90
84
  if (this._error)
91
85
  event.error = this._error;
92
- if (this._params)
93
- event.params = this._params;
86
+ if (this._labels)
87
+ event.context = { labels: this._labels };
94
88
  this._enqueue(event);
95
89
  }
96
90
  }
97
91
  exports.EventBuilder = EventBuilder;
98
92
  // Stringify an error code (ShareDB and Node errors carry one) for the
99
- // structured `error.code` field. Until 1.4.0 the code was appended to the
100
- // message text; a dedicated field is facetable downstream.
93
+ // structured `error.code` field a dedicated field is facetable downstream.
101
94
  function extractCode(code) {
102
95
  if (typeof code === 'string' || typeof code === 'number') {
103
96
  return String(code);
@@ -1,21 +1,43 @@
1
1
  import { EventBuilder } from './EventBuilder';
2
- import { JsonValue, LogClientOptions, PerfToken } from './types';
2
+ import { EndOptions, LogClientOptions, RecordOptions, StartOptions } from './types';
3
3
  export declare class LogClient {
4
4
  private _opts;
5
5
  private _eventBuffer;
6
- private _perfBuffer;
7
- private _activePerfs;
6
+ private _transactionBuffer;
7
+ private _spanBuffer;
8
+ private _activeSpans;
9
+ /** Per-launch id; the join key between records and this lifetime's origin. */
10
+ private readonly _lifetimeId;
11
+ /** JSON of the last origin sent, to detect changes (null ⇒ not yet sent). */
12
+ private _lastOriginJson;
8
13
  private _flushHandle;
9
14
  private _persistHandle;
10
15
  private _disposed;
11
16
  private _flushing;
12
17
  constructor(opts: LogClientOptions);
18
+ /** This LogClient's lifetime id (one per launch / process run). */
19
+ get lifetimeId(): string;
13
20
  event(type?: string): EventBuilder;
14
- startPerf(name: string, parent?: PerfToken): PerfToken;
15
- endPerf(token: PerfToken, context?: Record<string, JsonValue>): void;
21
+ /** Start a root timed operation (a trace root). Recorded as a transaction. */
22
+ startTransaction(name: string, opts?: StartOptions): Transaction;
23
+ /** Start a sub-operation under a transaction or span. Recorded as a span. */
24
+ startSpan(name: string, parent: Transaction | Span, opts?: StartOptions): Span;
25
+ /** Internal: end a live transaction/span by id (called by the handles). */
26
+ _end(id: string, opts?: EndOptions): void;
27
+ /** Record a complete root operation timed elsewhere → a transaction. */
28
+ recordTransaction(name: string, durationMs: number, opts?: RecordOptions): void;
29
+ /** Record a complete sub-operation timed elsewhere → a span under `parent`. */
30
+ recordSpan(name: string, durationMs: number, parent: Transaction | Span, opts?: RecordOptions): void;
31
+ private _belowThreshold;
32
+ private _buildOneShot;
16
33
  flush(): Promise<void>;
17
34
  dispose(): void;
18
35
  private _enqueueEvent;
36
+ private _enqueueTransaction;
37
+ private _enqueueSpan;
38
+ private _afterEnqueue;
39
+ /** The origin to attach to this flush, or undefined if unchanged since last sent. */
40
+ private _takeOriginIfChanged;
19
41
  private _sendInChunks;
20
42
  private _sendChunkWithRetry;
21
43
  private _sendChunk;
@@ -23,3 +45,27 @@ export declare class LogClient {
23
45
  private _persistNow;
24
46
  private _loadPersistedLogs;
25
47
  }
48
+ /** A live root operation. Recorded as a `transaction` when ended. */
49
+ export declare class Transaction {
50
+ private readonly _client;
51
+ readonly id: string;
52
+ readonly traceId: string;
53
+ readonly type: string;
54
+ constructor(_client: LogClient, id: string, traceId: string, type: string);
55
+ /** A transaction is its own trace root. */
56
+ get transactionId(): string;
57
+ startSpan(name: string, opts?: StartOptions): Span;
58
+ end(opts?: EndOptions): void;
59
+ }
60
+ /** A live sub-operation. Recorded as a `span` when ended. */
61
+ export declare class Span {
62
+ private readonly _client;
63
+ readonly id: string;
64
+ readonly traceId: string;
65
+ readonly transactionId: string;
66
+ readonly parentId: string;
67
+ readonly type: string;
68
+ constructor(_client: LogClient, id: string, traceId: string, transactionId: string, parentId: string, type: string);
69
+ startSpan(name: string, opts?: StartOptions): Span;
70
+ end(opts?: EndOptions): void;
71
+ }
package/dist/LogClient.js CHANGED
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LogClient = void 0;
3
+ exports.Span = exports.Transaction = exports.LogClient = void 0;
4
4
  const EventBuilder_1 = require("./EventBuilder");
5
5
  const util_1 = require("./util");
6
6
  // Level ordering for the optional getMinLevel gate. An event is dropped when
7
7
  // its level ranks below the host-supplied minimum.
8
8
  const LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
9
+ const DEFAULT_TYPE = 'app';
9
10
  // Defaults (all overridable via LogClientOptions)
10
11
  const DEFAULT_FLUSH_CADENCE_MS = 5000;
11
12
  const DEFAULT_MAX_BUFFER_SIZE = 100;
@@ -18,8 +19,13 @@ const PERSIST_DEBOUNCE_MS = 100;
18
19
  class LogClient {
19
20
  constructor(opts) {
20
21
  this._eventBuffer = [];
21
- this._perfBuffer = [];
22
- this._activePerfs = new Map();
22
+ this._transactionBuffer = [];
23
+ this._spanBuffer = [];
24
+ this._activeSpans = new Map();
25
+ /** Per-launch id; the join key between records and this lifetime's origin. */
26
+ this._lifetimeId = randomHex(16);
27
+ /** JSON of the last origin sent, to detect changes (null ⇒ not yet sent). */
28
+ this._lastOriginJson = null;
23
29
  this._flushHandle = null;
24
30
  this._persistHandle = null;
25
31
  this._disposed = false;
@@ -28,81 +34,120 @@ class LogClient {
28
34
  this._loadPersistedLogs();
29
35
  this._flushHandle = setInterval(() => this.flush(), opts.flushCadenceMs ?? DEFAULT_FLUSH_CADENCE_MS);
30
36
  }
37
+ /** This LogClient's lifetime id (one per launch / process run). */
38
+ get lifetimeId() {
39
+ return this._lifetimeId;
40
+ }
31
41
  // ---- Fluent event builder ----
32
42
  event(type = 'client-log') {
33
43
  return new EventBuilder_1.EventBuilder((evt) => this._enqueueEvent(evt), type);
34
44
  }
35
- // ---- Perf timing ----
36
- startPerf(name, parent) {
45
+ // ---- Live traces: transactions & spans ----
46
+ /** Start a root timed operation (a trace root). Recorded as a transaction. */
47
+ startTransaction(name, opts) {
37
48
  const id = randomHex(16);
38
- const trace_id = parent ? parent.trace_id : randomHex(32);
39
- const root_id = parent ? parent.root_id : id;
40
- const token = { id, trace_id, root_id, name };
41
- const active = {
42
- token,
43
- startTime: now(),
44
- tzOffset: (0, util_1.tzOffsetMinutes)(),
45
- parentToken: parent,
46
- children: [],
47
- };
48
- this._activePerfs.set(id, active);
49
- // Register as child of parent
50
- if (parent) {
51
- const parentActive = this._activePerfs.get(parent.id);
52
- if (parentActive) {
53
- parentActive.children.push(id);
54
- }
55
- }
56
- return token;
49
+ const trace_id = randomHex(32);
50
+ const type = opts?.type ?? DEFAULT_TYPE;
51
+ this._activeSpans.set(id, {
52
+ kind: 'transaction', id, trace_id, name, type,
53
+ startTime: now(), tzOffset: (0, util_1.tzOffsetMinutes)(), children: [],
54
+ });
55
+ return new Transaction(this, id, trace_id, type);
57
56
  }
58
- endPerf(token, context) {
59
- const active = this._activePerfs.get(token.id);
57
+ /** Start a sub-operation under a transaction or span. Recorded as a span. */
58
+ startSpan(name, parent, opts) {
59
+ const id = randomHex(16);
60
+ const type = opts?.type ?? DEFAULT_TYPE;
61
+ this._activeSpans.set(id, {
62
+ kind: 'span', id,
63
+ trace_id: parent.traceId,
64
+ transaction_id: parent.transactionId,
65
+ parent_id: parent.id,
66
+ name, type,
67
+ startTime: now(), tzOffset: (0, util_1.tzOffsetMinutes)(), children: [],
68
+ });
69
+ this._activeSpans.get(parent.id)?.children.push(id);
70
+ return new Span(this, id, parent.traceId, parent.transactionId, parent.id, type);
71
+ }
72
+ /** Internal: end a live transaction/span by id (called by the handles). */
73
+ _end(id, opts) {
74
+ const active = this._activeSpans.get(id);
60
75
  if (!active)
61
76
  return;
62
- const duration = now() - active.startTime;
63
- // Auto-close children that haven't been ended yet
77
+ // Auto-close any children not yet ended, so a forgotten child can't leak.
64
78
  for (const childId of active.children) {
65
- const childActive = this._activePerfs.get(childId);
66
- if (childActive) {
67
- this.endPerf(childActive.token);
68
- }
79
+ if (this._activeSpans.has(childId))
80
+ this._end(childId);
69
81
  }
70
- const perf = {
71
- id: token.id,
72
- trace_id: token.trace_id,
73
- root_id: token.root_id,
74
- name: token.name,
75
- type: 'client-perf',
76
- timestamp: Math.round(active.startTime),
82
+ const duration = Math.max(0, now() - active.startTime);
83
+ const base = {
84
+ id: active.id,
85
+ trace_id: active.trace_id,
86
+ name: active.name,
87
+ type: active.type,
88
+ timestamp: Math.round(active.startTime * 1000), // ms → µs
77
89
  duration: Math.round(duration),
78
- outcome: 'success',
90
+ outcome: opts?.outcome ?? 'success',
79
91
  tz_offset: active.tzOffset,
92
+ ...(opts?.labels && Object.keys(opts.labels).length > 0 ? { context: { labels: opts.labels } } : {}),
80
93
  };
81
- if (active.parentToken) {
82
- perf.parent_id = active.parentToken.id;
94
+ this._activeSpans.delete(id);
95
+ if (active.kind === 'transaction') {
96
+ this._enqueueTransaction(base);
83
97
  }
84
- if (context && Object.keys(context).length > 0) {
85
- perf.context = { tags: context };
86
- }
87
- this._perfBuffer.push(perf);
88
- this._activePerfs.delete(token.id);
89
- this._schedulePersist();
90
- // Force flush if buffer is getting large
91
- if (this._perfBuffer.length + this._eventBuffer.length >= (this._opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE)) {
92
- this.flush();
98
+ else {
99
+ this._enqueueSpan({ ...base, transaction_id: active.transaction_id, parent_id: active.parent_id });
93
100
  }
94
101
  }
102
+ // ---- One-shot (pre-measured) traces ----
103
+ /** Record a complete root operation timed elsewhere → a transaction. */
104
+ recordTransaction(name, durationMs, opts) {
105
+ if (this._disposed || this._belowThreshold(durationMs, opts))
106
+ return;
107
+ const id = randomHex(16);
108
+ this._enqueueTransaction(this._buildOneShot(id, randomHex(32), name, durationMs, opts));
109
+ }
110
+ /** Record a complete sub-operation timed elsewhere → a span under `parent`. */
111
+ recordSpan(name, durationMs, parent, opts) {
112
+ if (this._disposed || this._belowThreshold(durationMs, opts))
113
+ return;
114
+ const id = randomHex(16);
115
+ const rec = this._buildOneShot(id, parent.traceId, name, durationMs, opts);
116
+ rec.transaction_id = parent.transactionId;
117
+ rec.parent_id = parent.id;
118
+ this._enqueueSpan(rec);
119
+ }
120
+ _belowThreshold(durationMs, opts) {
121
+ return opts?.minDurationMs !== undefined && durationMs < opts.minDurationMs;
122
+ }
123
+ _buildOneShot(id, trace_id, name, durationMs, opts) {
124
+ const d = isFinite(durationMs) && durationMs > 0 ? durationMs : 0;
125
+ const rec = {
126
+ id, trace_id, name,
127
+ type: opts?.type ?? DEFAULT_TYPE,
128
+ // No live start, so back-compute it from the measured duration.
129
+ timestamp: Math.round((now() - d) * 1000), // ms → µs
130
+ duration: Math.round(d),
131
+ outcome: opts?.outcome ?? 'success',
132
+ tz_offset: (0, util_1.tzOffsetMinutes)(),
133
+ };
134
+ if (opts?.labels && Object.keys(opts.labels).length > 0)
135
+ rec.context = { labels: opts.labels };
136
+ return rec;
137
+ }
95
138
  // ---- Transport ----
96
139
  async flush() {
97
140
  if (this._disposed || this._flushing)
98
141
  return;
99
- if (this._eventBuffer.length === 0 && this._perfBuffer.length === 0)
142
+ if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0)
100
143
  return;
101
144
  this._flushing = true;
102
145
  try {
103
146
  const events = this._eventBuffer.splice(0);
104
- const perfs = this._perfBuffer.splice(0);
105
- await this._sendInChunks(events, perfs);
147
+ const transactions = this._transactionBuffer.splice(0);
148
+ const spans = this._spanBuffer.splice(0);
149
+ const origin = this._takeOriginIfChanged();
150
+ await this._sendInChunks(events, transactions, spans, origin);
106
151
  }
107
152
  finally {
108
153
  this._flushing = false;
@@ -121,109 +166,138 @@ class LogClient {
121
166
  clearTimeout(this._persistHandle);
122
167
  this._persistHandle = null;
123
168
  }
124
- // Persist anything remaining
125
169
  this._persistNow();
126
170
  }
127
- // ---- Internal: event buffering ----
171
+ // ---- Internal: buffering ----
128
172
  _enqueueEvent(event) {
129
173
  if (this._disposed)
130
174
  return;
131
- // Level gate: drop events below the host-supplied minimum before they ever
132
- // buffer, persist, or ship. No callback ⇒ emit everything (back-compat).
175
+ // Level gate: drop events below the host-supplied minimum before buffering.
133
176
  const minLevel = this._opts.getMinLevel?.();
134
177
  if (minLevel && LEVEL_RANK[event.level] < LEVEL_RANK[minLevel])
135
178
  return;
179
+ if (event.locale === undefined) {
180
+ const locale = this._opts.getLocale?.();
181
+ if (locale)
182
+ event.locale = locale;
183
+ }
136
184
  this._eventBuffer.push(event);
185
+ this._afterEnqueue();
186
+ }
187
+ _enqueueTransaction(rec) {
188
+ if (this._disposed)
189
+ return;
190
+ this._transactionBuffer.push(rec);
191
+ this._afterEnqueue();
192
+ }
193
+ _enqueueSpan(rec) {
194
+ if (this._disposed)
195
+ return;
196
+ this._spanBuffer.push(rec);
197
+ this._afterEnqueue();
198
+ }
199
+ _afterEnqueue() {
137
200
  this._schedulePersist();
138
- if (this._eventBuffer.length >= (this._opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE)) {
201
+ const total = this._eventBuffer.length + this._transactionBuffer.length + this._spanBuffer.length;
202
+ if (total >= (this._opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE))
139
203
  this.flush();
204
+ }
205
+ /** The origin to attach to this flush, or undefined if unchanged since last sent. */
206
+ _takeOriginIfChanged() {
207
+ let origin;
208
+ try {
209
+ origin = this._opts.getOrigin();
210
+ }
211
+ catch {
212
+ return undefined;
140
213
  }
214
+ const oj = JSON.stringify(origin);
215
+ if (oj === this._lastOriginJson)
216
+ return undefined;
217
+ this._lastOriginJson = oj;
218
+ return { ...origin, lifetime_id: this._lifetimeId };
141
219
  }
142
220
  // ---- Internal: chunked sending ----
143
- async _sendInChunks(events, perfs) {
221
+ async _sendInChunks(events, transactions, spans, origin) {
144
222
  const maxChunkSize = this._opts.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;
145
223
  const maxChunkBytes = this._opts.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
146
- // Combine events and perfs into chunks that respect size limits
147
- let eventIdx = 0;
148
- let perfIdx = 0;
149
- let isFirstChunk = true;
150
- while (eventIdx < events.length || perfIdx < perfs.length) {
151
- if (!isFirstChunk) {
224
+ const work = [
225
+ ...events.map((rec) => ({ k: 'e', rec })),
226
+ ...transactions.map((rec) => ({ k: 't', rec })),
227
+ ...spans.map((rec) => ({ k: 's', rec })),
228
+ ];
229
+ let i = 0;
230
+ let isFirst = true;
231
+ while (i < work.length || (isFirst && origin)) {
232
+ if (!isFirst)
152
233
  await delay(INTER_CHUNK_DELAY_MS);
153
- }
154
- isFirstChunk = false;
155
- const chunkEvents = [];
156
- const chunkPerfs = [];
157
- let estimatedBytes = 200; // base overhead for batch envelope
158
- // Fill chunk with events
159
- while (eventIdx < events.length && chunkEvents.length + chunkPerfs.length < maxChunkSize) {
160
- const itemBytes = estimateJsonSize(events[eventIdx]);
161
- if (estimatedBytes + itemBytes > maxChunkBytes && chunkEvents.length > 0)
162
- break;
163
- chunkEvents.push(events[eventIdx]);
164
- estimatedBytes += itemBytes;
165
- eventIdx++;
166
- }
167
- // Fill chunk with perfs
168
- while (perfIdx < perfs.length && chunkEvents.length + chunkPerfs.length < maxChunkSize) {
169
- const itemBytes = estimateJsonSize(perfs[perfIdx]);
170
- if (estimatedBytes + itemBytes > maxChunkBytes && (chunkEvents.length + chunkPerfs.length) > 0)
234
+ const ce = [];
235
+ const ct = [];
236
+ const cs = [];
237
+ let bytes = 200; // base overhead for the batch envelope
238
+ while (i < work.length && (ce.length + ct.length + cs.length) < maxChunkSize) {
239
+ const item = work[i];
240
+ const itemBytes = estimateJsonSize(item.rec);
241
+ if (bytes + itemBytes > maxChunkBytes && (ce.length + ct.length + cs.length) > 0)
171
242
  break;
172
- chunkPerfs.push(perfs[perfIdx]);
173
- estimatedBytes += itemBytes;
174
- perfIdx++;
243
+ if (item.k === 'e')
244
+ ce.push(item.rec);
245
+ else if (item.k === 't')
246
+ ct.push(item.rec);
247
+ else
248
+ cs.push(item.rec);
249
+ bytes += itemBytes;
250
+ i++;
175
251
  }
176
- if (chunkEvents.length === 0 && chunkPerfs.length === 0)
252
+ const chunkOrigin = isFirst ? origin : undefined;
253
+ isFirst = false;
254
+ if (ce.length === 0 && ct.length === 0 && cs.length === 0 && !chunkOrigin)
177
255
  break;
178
- await this._sendChunkWithRetry(chunkEvents, chunkPerfs);
256
+ await this._sendChunkWithRetry(ce, ct, cs, chunkOrigin);
179
257
  }
180
258
  }
181
- async _sendChunkWithRetry(events, perfs) {
259
+ async _sendChunkWithRetry(events, transactions, spans, origin) {
182
260
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
183
261
  try {
184
- await this._sendChunk(events, perfs);
262
+ await this._sendChunk(events, transactions, spans, origin);
185
263
  return;
186
264
  }
187
- catch (err) {
265
+ catch {
188
266
  if (attempt < MAX_RETRIES) {
189
267
  await delay(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
190
268
  }
191
269
  else {
192
- // Max retries exceeded — put items back for persistence
270
+ // Max retries exceeded — put records back for persistence/retry, and
271
+ // re-arm the origin so it ships again on the next successful flush.
193
272
  this._eventBuffer.push(...events);
194
- this._perfBuffer.push(...perfs);
273
+ this._transactionBuffer.push(...transactions);
274
+ this._spanBuffer.push(...spans);
275
+ if (origin)
276
+ this._lastOriginJson = null;
195
277
  this._schedulePersist();
196
278
  }
197
279
  }
198
280
  }
199
281
  }
200
- async _sendChunk(events, perfs) {
201
- const batch = {
202
- client: this._opts.client,
203
- events,
204
- perfs,
205
- };
282
+ async _sendChunk(events, transactions, spans, origin) {
283
+ const batch = { events, transactions, spans };
284
+ batch.lifetime_id = this._lifetimeId;
206
285
  const userId = this._opts.getUserId?.();
207
- const sessionRef = this._opts.getSessionRef?.();
208
286
  const deviceId = this._opts.getDeviceId?.();
209
287
  if (userId)
210
288
  batch.user_id = userId;
211
- if (sessionRef)
212
- batch.session_ref = sessionRef;
213
289
  if (deviceId)
214
290
  batch.device_id = deviceId;
291
+ if (origin)
292
+ batch.origin = origin;
215
293
  const headers = await Promise.resolve(this._opts.getAuthHeaders());
216
294
  const response = await fetch(this._opts.endpoint, {
217
295
  method: 'POST',
218
- headers: {
219
- 'Content-Type': 'application/json',
220
- ...headers,
221
- },
296
+ headers: { 'Content-Type': 'application/json', ...headers },
222
297
  body: JSON.stringify(batch),
223
298
  });
224
- if (!response.ok) {
299
+ if (!response.ok)
225
300
  throw new Error(`Server returned ${response.status}`);
226
- }
227
301
  }
228
302
  // ---- Internal: persistence ----
229
303
  _schedulePersist() {
@@ -237,13 +311,14 @@ class LogClient {
237
311
  _persistNow() {
238
312
  if (!this._opts.persistLogs)
239
313
  return;
240
- if (this._eventBuffer.length === 0 && this._perfBuffer.length === 0) {
314
+ if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0) {
241
315
  this._opts.persistLogs('').catch(() => { });
242
316
  return;
243
317
  }
244
318
  const data = JSON.stringify({
245
319
  events: this._eventBuffer,
246
- perfs: this._perfBuffer,
320
+ transactions: this._transactionBuffer,
321
+ spans: this._spanBuffer,
247
322
  });
248
323
  this._opts.persistLogs(data).catch(() => { });
249
324
  }
@@ -255,13 +330,12 @@ class LogClient {
255
330
  if (!data)
256
331
  return;
257
332
  const parsed = JSON.parse(data);
258
- if (Array.isArray(parsed.events)) {
333
+ if (Array.isArray(parsed.events))
259
334
  this._eventBuffer.push(...parsed.events);
260
- }
261
- if (Array.isArray(parsed.perfs)) {
262
- this._perfBuffer.push(...parsed.perfs);
263
- }
264
- // Clear persisted data now that it's loaded
335
+ if (Array.isArray(parsed.transactions))
336
+ this._transactionBuffer.push(...parsed.transactions);
337
+ if (Array.isArray(parsed.spans))
338
+ this._spanBuffer.push(...parsed.spans);
265
339
  this._opts.persistLogs?.('').catch(() => { });
266
340
  }
267
341
  catch {
@@ -270,6 +344,35 @@ class LogClient {
270
344
  }
271
345
  }
272
346
  exports.LogClient = LogClient;
347
+ // ---- Trace handles ----
348
+ /** A live root operation. Recorded as a `transaction` when ended. */
349
+ class Transaction {
350
+ constructor(_client, id, traceId, type) {
351
+ this._client = _client;
352
+ this.id = id;
353
+ this.traceId = traceId;
354
+ this.type = type;
355
+ }
356
+ /** A transaction is its own trace root. */
357
+ get transactionId() { return this.id; }
358
+ startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
359
+ end(opts) { this._client._end(this.id, opts); }
360
+ }
361
+ exports.Transaction = Transaction;
362
+ /** A live sub-operation. Recorded as a `span` when ended. */
363
+ class Span {
364
+ constructor(_client, id, traceId, transactionId, parentId, type) {
365
+ this._client = _client;
366
+ this.id = id;
367
+ this.traceId = traceId;
368
+ this.transactionId = transactionId;
369
+ this.parentId = parentId;
370
+ this.type = type;
371
+ }
372
+ startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
373
+ end(opts) { this._client._end(this.id, opts); }
374
+ }
375
+ exports.Span = Span;
273
376
  // ---- Helpers ----
274
377
  function randomHex(length) {
275
378
  const chars = '0123456789abcdef';
@@ -281,8 +384,6 @@ function randomHex(length) {
281
384
  }
282
385
  function now() {
283
386
  if (typeof performance !== 'undefined' && performance.now) {
284
- // Use performance.now() for high-res timing, but we need wall-clock for timestamps
285
- // Store the offset on first call
286
387
  if (!now._offset) {
287
388
  now._offset = Date.now() - performance.now();
288
389
  }
@@ -294,6 +395,5 @@ function delay(ms) {
294
395
  return new Promise(resolve => setTimeout(resolve, ms));
295
396
  }
296
397
  function estimateJsonSize(obj) {
297
- // Fast estimate — avoid full serialization during chunking
298
398
  return JSON.stringify(obj).length;
299
399
  }
@@ -0,0 +1,71 @@
1
+ import { EventBuilder } from './EventBuilder';
2
+ import { EndOptions, LogClientOptions, RecordOptions, StartOptions } from './types';
3
+ export declare class TracelogClient {
4
+ private _opts;
5
+ private _eventBuffer;
6
+ private _transactionBuffer;
7
+ private _spanBuffer;
8
+ private _activeSpans;
9
+ /** Per-launch id; the join key between records and this lifetime's origin. */
10
+ private readonly _lifetimeId;
11
+ /** JSON of the last origin sent, to detect changes (null ⇒ not yet sent). */
12
+ private _lastOriginJson;
13
+ private _flushHandle;
14
+ private _persistHandle;
15
+ private _disposed;
16
+ private _flushing;
17
+ constructor(opts: LogClientOptions);
18
+ /** This TracelogClient's lifetime id (one per launch / process run). */
19
+ get lifetimeId(): string;
20
+ event(type?: string): EventBuilder;
21
+ /** Start a root timed operation (a trace root). Recorded as a transaction. */
22
+ startTransaction(name: string, opts?: StartOptions): Transaction;
23
+ /** Start a sub-operation under a transaction or span. Recorded as a span. */
24
+ startSpan(name: string, parent: Transaction | Span, opts?: StartOptions): Span;
25
+ /** Internal: end a live transaction/span by id (called by the handles). */
26
+ _end(id: string, opts?: EndOptions): void;
27
+ /** Record a complete root operation timed elsewhere → a transaction. */
28
+ recordTransaction(name: string, durationMs: number, opts?: RecordOptions): void;
29
+ /** Record a complete sub-operation timed elsewhere → a span under `parent`. */
30
+ recordSpan(name: string, durationMs: number, parent: Transaction | Span, opts?: RecordOptions): void;
31
+ private _belowThreshold;
32
+ private _buildOneShot;
33
+ flush(): Promise<void>;
34
+ dispose(): void;
35
+ private _enqueueEvent;
36
+ private _enqueueTransaction;
37
+ private _enqueueSpan;
38
+ private _afterEnqueue;
39
+ /** The origin to attach to this flush, or undefined if unchanged since last sent. */
40
+ private _takeOriginIfChanged;
41
+ private _sendInChunks;
42
+ private _sendChunkWithRetry;
43
+ private _sendChunk;
44
+ private _schedulePersist;
45
+ private _persistNow;
46
+ private _loadPersistedLogs;
47
+ }
48
+ /** A live root operation. Recorded as a `transaction` when ended. */
49
+ export declare class Transaction {
50
+ private readonly _client;
51
+ readonly id: string;
52
+ readonly traceId: string;
53
+ readonly type: string;
54
+ constructor(_client: TracelogClient, id: string, traceId: string, type: string);
55
+ /** A transaction is its own trace root. */
56
+ get transactionId(): string;
57
+ startSpan(name: string, opts?: StartOptions): Span;
58
+ end(opts?: EndOptions): void;
59
+ }
60
+ /** A live sub-operation. Recorded as a `span` when ended. */
61
+ export declare class Span {
62
+ private readonly _client;
63
+ readonly id: string;
64
+ readonly traceId: string;
65
+ readonly transactionId: string;
66
+ readonly parentId: string;
67
+ readonly type: string;
68
+ constructor(_client: TracelogClient, id: string, traceId: string, transactionId: string, parentId: string, type: string);
69
+ startSpan(name: string, opts?: StartOptions): Span;
70
+ end(opts?: EndOptions): void;
71
+ }
@@ -0,0 +1,399 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Span = exports.Transaction = exports.TracelogClient = void 0;
4
+ const EventBuilder_1 = require("./EventBuilder");
5
+ const util_1 = require("./util");
6
+ // Level ordering for the optional getMinLevel gate. An event is dropped when
7
+ // its level ranks below the host-supplied minimum.
8
+ const LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
9
+ const DEFAULT_TYPE = 'app';
10
+ // Defaults (all overridable via LogClientOptions)
11
+ const DEFAULT_FLUSH_CADENCE_MS = 5000;
12
+ const DEFAULT_MAX_BUFFER_SIZE = 100;
13
+ const DEFAULT_MAX_CHUNK_SIZE = 50;
14
+ const DEFAULT_MAX_CHUNK_BYTES = 512 * 1024;
15
+ const INTER_CHUNK_DELAY_MS = 200;
16
+ const MAX_RETRIES = 3;
17
+ const BASE_RETRY_DELAY_MS = 1000;
18
+ const PERSIST_DEBOUNCE_MS = 100;
19
+ class TracelogClient {
20
+ constructor(opts) {
21
+ this._eventBuffer = [];
22
+ this._transactionBuffer = [];
23
+ this._spanBuffer = [];
24
+ this._activeSpans = new Map();
25
+ /** Per-launch id; the join key between records and this lifetime's origin. */
26
+ this._lifetimeId = randomHex(16);
27
+ /** JSON of the last origin sent, to detect changes (null ⇒ not yet sent). */
28
+ this._lastOriginJson = null;
29
+ this._flushHandle = null;
30
+ this._persistHandle = null;
31
+ this._disposed = false;
32
+ this._flushing = false;
33
+ this._opts = opts;
34
+ this._loadPersistedLogs();
35
+ this._flushHandle = setInterval(() => this.flush(), opts.flushCadenceMs ?? DEFAULT_FLUSH_CADENCE_MS);
36
+ }
37
+ /** This TracelogClient's lifetime id (one per launch / process run). */
38
+ get lifetimeId() {
39
+ return this._lifetimeId;
40
+ }
41
+ // ---- Fluent event builder ----
42
+ event(type = 'client-log') {
43
+ return new EventBuilder_1.EventBuilder((evt) => this._enqueueEvent(evt), type);
44
+ }
45
+ // ---- Live traces: transactions & spans ----
46
+ /** Start a root timed operation (a trace root). Recorded as a transaction. */
47
+ startTransaction(name, opts) {
48
+ const id = randomHex(16);
49
+ const trace_id = randomHex(32);
50
+ const type = opts?.type ?? DEFAULT_TYPE;
51
+ this._activeSpans.set(id, {
52
+ kind: 'transaction', id, trace_id, name, type,
53
+ startTime: now(), tzOffset: (0, util_1.tzOffsetMinutes)(), children: [],
54
+ });
55
+ return new Transaction(this, id, trace_id, type);
56
+ }
57
+ /** Start a sub-operation under a transaction or span. Recorded as a span. */
58
+ startSpan(name, parent, opts) {
59
+ const id = randomHex(16);
60
+ const type = opts?.type ?? DEFAULT_TYPE;
61
+ this._activeSpans.set(id, {
62
+ kind: 'span', id,
63
+ trace_id: parent.traceId,
64
+ transaction_id: parent.transactionId,
65
+ parent_id: parent.id,
66
+ name, type,
67
+ startTime: now(), tzOffset: (0, util_1.tzOffsetMinutes)(), children: [],
68
+ });
69
+ this._activeSpans.get(parent.id)?.children.push(id);
70
+ return new Span(this, id, parent.traceId, parent.transactionId, parent.id, type);
71
+ }
72
+ /** Internal: end a live transaction/span by id (called by the handles). */
73
+ _end(id, opts) {
74
+ const active = this._activeSpans.get(id);
75
+ if (!active)
76
+ return;
77
+ // Auto-close any children not yet ended, so a forgotten child can't leak.
78
+ for (const childId of active.children) {
79
+ if (this._activeSpans.has(childId))
80
+ this._end(childId);
81
+ }
82
+ const duration = Math.max(0, now() - active.startTime);
83
+ const base = {
84
+ id: active.id,
85
+ trace_id: active.trace_id,
86
+ name: active.name,
87
+ type: active.type,
88
+ timestamp: Math.round(active.startTime * 1000), // ms → µs
89
+ duration: Math.round(duration),
90
+ outcome: opts?.outcome ?? 'success',
91
+ tz_offset: active.tzOffset,
92
+ ...(opts?.labels && Object.keys(opts.labels).length > 0 ? { context: { labels: opts.labels } } : {}),
93
+ };
94
+ this._activeSpans.delete(id);
95
+ if (active.kind === 'transaction') {
96
+ this._enqueueTransaction(base);
97
+ }
98
+ else {
99
+ this._enqueueSpan({ ...base, transaction_id: active.transaction_id, parent_id: active.parent_id });
100
+ }
101
+ }
102
+ // ---- One-shot (pre-measured) traces ----
103
+ /** Record a complete root operation timed elsewhere → a transaction. */
104
+ recordTransaction(name, durationMs, opts) {
105
+ if (this._disposed || this._belowThreshold(durationMs, opts))
106
+ return;
107
+ const id = randomHex(16);
108
+ this._enqueueTransaction(this._buildOneShot(id, randomHex(32), name, durationMs, opts));
109
+ }
110
+ /** Record a complete sub-operation timed elsewhere → a span under `parent`. */
111
+ recordSpan(name, durationMs, parent, opts) {
112
+ if (this._disposed || this._belowThreshold(durationMs, opts))
113
+ return;
114
+ const id = randomHex(16);
115
+ const rec = this._buildOneShot(id, parent.traceId, name, durationMs, opts);
116
+ rec.transaction_id = parent.transactionId;
117
+ rec.parent_id = parent.id;
118
+ this._enqueueSpan(rec);
119
+ }
120
+ _belowThreshold(durationMs, opts) {
121
+ return opts?.minDurationMs !== undefined && durationMs < opts.minDurationMs;
122
+ }
123
+ _buildOneShot(id, trace_id, name, durationMs, opts) {
124
+ const d = isFinite(durationMs) && durationMs > 0 ? durationMs : 0;
125
+ const rec = {
126
+ id, trace_id, name,
127
+ type: opts?.type ?? DEFAULT_TYPE,
128
+ // No live start, so back-compute it from the measured duration.
129
+ timestamp: Math.round((now() - d) * 1000), // ms → µs
130
+ duration: Math.round(d),
131
+ outcome: opts?.outcome ?? 'success',
132
+ tz_offset: (0, util_1.tzOffsetMinutes)(),
133
+ };
134
+ if (opts?.labels && Object.keys(opts.labels).length > 0)
135
+ rec.context = { labels: opts.labels };
136
+ return rec;
137
+ }
138
+ // ---- Transport ----
139
+ async flush() {
140
+ if (this._disposed || this._flushing)
141
+ return;
142
+ if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0)
143
+ return;
144
+ this._flushing = true;
145
+ try {
146
+ const events = this._eventBuffer.splice(0);
147
+ const transactions = this._transactionBuffer.splice(0);
148
+ const spans = this._spanBuffer.splice(0);
149
+ const origin = this._takeOriginIfChanged();
150
+ await this._sendInChunks(events, transactions, spans, origin);
151
+ }
152
+ finally {
153
+ this._flushing = false;
154
+ this._schedulePersist();
155
+ }
156
+ }
157
+ dispose() {
158
+ if (this._disposed)
159
+ return;
160
+ this._disposed = true;
161
+ if (this._flushHandle) {
162
+ clearInterval(this._flushHandle);
163
+ this._flushHandle = null;
164
+ }
165
+ if (this._persistHandle) {
166
+ clearTimeout(this._persistHandle);
167
+ this._persistHandle = null;
168
+ }
169
+ this._persistNow();
170
+ }
171
+ // ---- Internal: buffering ----
172
+ _enqueueEvent(event) {
173
+ if (this._disposed)
174
+ return;
175
+ // Level gate: drop events below the host-supplied minimum before buffering.
176
+ const minLevel = this._opts.getMinLevel?.();
177
+ if (minLevel && LEVEL_RANK[event.level] < LEVEL_RANK[minLevel])
178
+ return;
179
+ if (event.locale === undefined) {
180
+ const locale = this._opts.getLocale?.();
181
+ if (locale)
182
+ event.locale = locale;
183
+ }
184
+ this._eventBuffer.push(event);
185
+ this._afterEnqueue();
186
+ }
187
+ _enqueueTransaction(rec) {
188
+ if (this._disposed)
189
+ return;
190
+ this._transactionBuffer.push(rec);
191
+ this._afterEnqueue();
192
+ }
193
+ _enqueueSpan(rec) {
194
+ if (this._disposed)
195
+ return;
196
+ this._spanBuffer.push(rec);
197
+ this._afterEnqueue();
198
+ }
199
+ _afterEnqueue() {
200
+ this._schedulePersist();
201
+ const total = this._eventBuffer.length + this._transactionBuffer.length + this._spanBuffer.length;
202
+ if (total >= (this._opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE))
203
+ this.flush();
204
+ }
205
+ /** The origin to attach to this flush, or undefined if unchanged since last sent. */
206
+ _takeOriginIfChanged() {
207
+ let origin;
208
+ try {
209
+ origin = this._opts.getOrigin();
210
+ }
211
+ catch {
212
+ return undefined;
213
+ }
214
+ const oj = JSON.stringify(origin);
215
+ if (oj === this._lastOriginJson)
216
+ return undefined;
217
+ this._lastOriginJson = oj;
218
+ return { ...origin, lifetime_id: this._lifetimeId };
219
+ }
220
+ // ---- Internal: chunked sending ----
221
+ async _sendInChunks(events, transactions, spans, origin) {
222
+ const maxChunkSize = this._opts.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;
223
+ const maxChunkBytes = this._opts.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
224
+ const work = [
225
+ ...events.map((rec) => ({ k: 'e', rec })),
226
+ ...transactions.map((rec) => ({ k: 't', rec })),
227
+ ...spans.map((rec) => ({ k: 's', rec })),
228
+ ];
229
+ let i = 0;
230
+ let isFirst = true;
231
+ while (i < work.length || (isFirst && origin)) {
232
+ if (!isFirst)
233
+ await delay(INTER_CHUNK_DELAY_MS);
234
+ const ce = [];
235
+ const ct = [];
236
+ const cs = [];
237
+ let bytes = 200; // base overhead for the batch envelope
238
+ while (i < work.length && (ce.length + ct.length + cs.length) < maxChunkSize) {
239
+ const item = work[i];
240
+ const itemBytes = estimateJsonSize(item.rec);
241
+ if (bytes + itemBytes > maxChunkBytes && (ce.length + ct.length + cs.length) > 0)
242
+ break;
243
+ if (item.k === 'e')
244
+ ce.push(item.rec);
245
+ else if (item.k === 't')
246
+ ct.push(item.rec);
247
+ else
248
+ cs.push(item.rec);
249
+ bytes += itemBytes;
250
+ i++;
251
+ }
252
+ const chunkOrigin = isFirst ? origin : undefined;
253
+ isFirst = false;
254
+ if (ce.length === 0 && ct.length === 0 && cs.length === 0 && !chunkOrigin)
255
+ break;
256
+ await this._sendChunkWithRetry(ce, ct, cs, chunkOrigin);
257
+ }
258
+ }
259
+ async _sendChunkWithRetry(events, transactions, spans, origin) {
260
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
261
+ try {
262
+ await this._sendChunk(events, transactions, spans, origin);
263
+ return;
264
+ }
265
+ catch {
266
+ if (attempt < MAX_RETRIES) {
267
+ await delay(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
268
+ }
269
+ else {
270
+ // Max retries exceeded — put records back for persistence/retry, and
271
+ // re-arm the origin so it ships again on the next successful flush.
272
+ this._eventBuffer.push(...events);
273
+ this._transactionBuffer.push(...transactions);
274
+ this._spanBuffer.push(...spans);
275
+ if (origin)
276
+ this._lastOriginJson = null;
277
+ this._schedulePersist();
278
+ }
279
+ }
280
+ }
281
+ }
282
+ async _sendChunk(events, transactions, spans, origin) {
283
+ const batch = { events, transactions, spans };
284
+ batch.lifetime_id = this._lifetimeId;
285
+ const userId = this._opts.getUserId?.();
286
+ const deviceId = this._opts.getDeviceId?.();
287
+ if (userId)
288
+ batch.user_id = userId;
289
+ if (deviceId)
290
+ batch.device_id = deviceId;
291
+ if (origin)
292
+ batch.origin = origin;
293
+ const headers = await Promise.resolve(this._opts.getAuthHeaders());
294
+ const response = await fetch(this._opts.endpoint, {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json', ...headers },
297
+ body: JSON.stringify(batch),
298
+ });
299
+ if (!response.ok)
300
+ throw new Error(`Server returned ${response.status}`);
301
+ }
302
+ // ---- Internal: persistence ----
303
+ _schedulePersist() {
304
+ if (this._persistHandle || !this._opts.persistLogs)
305
+ return;
306
+ this._persistHandle = setTimeout(() => {
307
+ this._persistHandle = null;
308
+ this._persistNow();
309
+ }, PERSIST_DEBOUNCE_MS);
310
+ }
311
+ _persistNow() {
312
+ if (!this._opts.persistLogs)
313
+ return;
314
+ if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0) {
315
+ this._opts.persistLogs('').catch(() => { });
316
+ return;
317
+ }
318
+ const data = JSON.stringify({
319
+ events: this._eventBuffer,
320
+ transactions: this._transactionBuffer,
321
+ spans: this._spanBuffer,
322
+ });
323
+ this._opts.persistLogs(data).catch(() => { });
324
+ }
325
+ async _loadPersistedLogs() {
326
+ if (!this._opts.loadPersistedLogs)
327
+ return;
328
+ try {
329
+ const data = await this._opts.loadPersistedLogs();
330
+ if (!data)
331
+ return;
332
+ const parsed = JSON.parse(data);
333
+ if (Array.isArray(parsed.events))
334
+ this._eventBuffer.push(...parsed.events);
335
+ if (Array.isArray(parsed.transactions))
336
+ this._transactionBuffer.push(...parsed.transactions);
337
+ if (Array.isArray(parsed.spans))
338
+ this._spanBuffer.push(...parsed.spans);
339
+ this._opts.persistLogs?.('').catch(() => { });
340
+ }
341
+ catch {
342
+ // Ignore parse errors from corrupted persisted data
343
+ }
344
+ }
345
+ }
346
+ exports.TracelogClient = TracelogClient;
347
+ // ---- Trace handles ----
348
+ /** A live root operation. Recorded as a `transaction` when ended. */
349
+ class Transaction {
350
+ constructor(_client, id, traceId, type) {
351
+ this._client = _client;
352
+ this.id = id;
353
+ this.traceId = traceId;
354
+ this.type = type;
355
+ }
356
+ /** A transaction is its own trace root. */
357
+ get transactionId() { return this.id; }
358
+ startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
359
+ end(opts) { this._client._end(this.id, opts); }
360
+ }
361
+ exports.Transaction = Transaction;
362
+ /** A live sub-operation. Recorded as a `span` when ended. */
363
+ class Span {
364
+ constructor(_client, id, traceId, transactionId, parentId, type) {
365
+ this._client = _client;
366
+ this.id = id;
367
+ this.traceId = traceId;
368
+ this.transactionId = transactionId;
369
+ this.parentId = parentId;
370
+ this.type = type;
371
+ }
372
+ startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
373
+ end(opts) { this._client._end(this.id, opts); }
374
+ }
375
+ exports.Span = Span;
376
+ // ---- Helpers ----
377
+ function randomHex(length) {
378
+ const chars = '0123456789abcdef';
379
+ let result = '';
380
+ for (let i = 0; i < length; i++) {
381
+ result += chars[Math.floor(Math.random() * 16)];
382
+ }
383
+ return result;
384
+ }
385
+ function now() {
386
+ if (typeof performance !== 'undefined' && performance.now) {
387
+ if (!now._offset) {
388
+ now._offset = Date.now() - performance.now();
389
+ }
390
+ return now._offset + performance.now();
391
+ }
392
+ return Date.now();
393
+ }
394
+ function delay(ms) {
395
+ return new Promise(resolve => setTimeout(resolve, ms));
396
+ }
397
+ function estimateJsonSize(obj) {
398
+ return JSON.stringify(obj).length;
399
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { LogClient } from './LogClient';
1
+ export { TracelogClient, Transaction, Span } from './TracelogClient';
2
2
  export { EventBuilder } from './EventBuilder';
3
- export { LogBatch, LogEventItem, LogPerfItem, ClientInfo, PerfToken, LogClientOptions, LogLevel, JsonValue, } from './types';
3
+ export { RecordBatch, EventRecord, TransactionRecord, SpanRecord, RecordContext, RecordOrigin, RecordKind, Outcome, StartOptions, EndOptions, RecordOptions, LogClientOptions, LogLevel, JsonValue, } from './types';
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EventBuilder = exports.LogClient = void 0;
4
- var LogClient_1 = require("./LogClient");
5
- Object.defineProperty(exports, "LogClient", { enumerable: true, get: function () { return LogClient_1.LogClient; } });
3
+ exports.EventBuilder = exports.Span = exports.Transaction = exports.TracelogClient = void 0;
4
+ var TracelogClient_1 = require("./TracelogClient");
5
+ Object.defineProperty(exports, "TracelogClient", { enumerable: true, get: function () { return TracelogClient_1.TracelogClient; } });
6
+ Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return TracelogClient_1.Transaction; } });
7
+ Object.defineProperty(exports, "Span", { enumerable: true, get: function () { return TracelogClient_1.Span; } });
6
8
  var EventBuilder_1 = require("./EventBuilder");
7
9
  Object.defineProperty(exports, "EventBuilder", { enumerable: true, get: function () { return EventBuilder_1.EventBuilder; } });
package/dist/types.d.ts CHANGED
@@ -1,45 +1,58 @@
1
- import type { ClientInfo, LogLevel } from '@redthreadlabs/tracelog-schema';
2
- export type { JsonValue, LogLevel, LogBatch, LogEventItem, LogPerfItem, ClientInfo, } from '@redthreadlabs/tracelog-schema';
3
- export interface PerfToken {
4
- /** 16-char hex ID for this perf */
5
- id: string;
6
- /** 32-char hex trace ID (shared across the entire perf tree) */
7
- trace_id: string;
8
- /** ID of the root perf in this trace */
9
- root_id: string;
10
- /** Name of the operation being measured */
11
- name: string;
1
+ import type { JsonValue, LogLevel, RecordOrigin } from '@redthreadlabs/tracelog-schema';
2
+ export type { JsonValue, LogLevel, RecordBatch, EventRecord, TransactionRecord, SpanRecord, RecordContext, RecordOrigin, RecordKind, } from '@redthreadlabs/tracelog-schema';
3
+ export type Outcome = 'success' | 'failure' | 'unknown';
4
+ /** Options when starting a live transaction or span. */
5
+ export interface StartOptions {
6
+ /** Span/transaction type, e.g. 'db', 'app'. Default: 'app'. */
7
+ type?: string;
8
+ }
9
+ /** Options when ending a live transaction or span. */
10
+ export interface EndOptions {
11
+ labels?: Record<string, JsonValue>;
12
+ outcome?: Outcome;
13
+ }
14
+ /** Options when recording a pre-measured transaction or span in one call. */
15
+ export interface RecordOptions {
16
+ type?: string;
17
+ labels?: Record<string, JsonValue>;
18
+ outcome?: Outcome;
19
+ /** Drop the record if its duration is below this (ms). */
20
+ minDurationMs?: number;
12
21
  }
13
22
  export interface LogClientOptions {
14
23
  /** Server endpoint URL for log submission */
15
24
  endpoint: string;
16
25
  /** Returns auth headers (e.g. { Authorization: 'Bearer ...' }) */
17
26
  getAuthHeaders: () => Promise<Record<string, string>> | Record<string, string>;
18
- /** Static client/device info, sent once per batch */
19
- client: ClientInfo;
20
- /** Returns current user ID, if logged in */
27
+ /**
28
+ * The current RecordOrigin (service + environment). The SDK sends it as a
29
+ * `metadata` record on the first batch of this lifetime and again whenever it
30
+ * changes; it fills in `lifetime_id`.
31
+ */
32
+ getOrigin: () => RecordOrigin;
33
+ /** Returns current user ID, if logged in (→ batch.user_id). */
21
34
  getUserId?: () => string | undefined;
22
- /** Returns current session reference */
23
- getSessionRef?: () => string | undefined;
24
- /** Returns native device identifier */
35
+ /** Returns the consumer's opaque device/installation id (→ batch.device_id). */
25
36
  getDeviceId?: () => string | undefined;
37
+ /** Returns the UI locale at event time; stamped onto each event. */
38
+ getLocale?: () => string | undefined;
26
39
  /**
27
40
  * Returns the minimum level to emit. Events whose level ranks below this
28
41
  * (debug < info < warn < error) are dropped before buffering — they never
29
42
  * persist or ship. Called once per event, so keep it cheap and synchronous.
30
- * Omit to emit every level (default). Perfs are unaffected.
43
+ * Omit to emit every level (default). Transactions/spans are unaffected.
31
44
  */
32
45
  getMinLevel?: () => LogLevel;
33
46
  /** Flush cadence in ms. Default: 5000 */
34
47
  flushCadenceMs?: number;
35
- /** Max events buffered before forced flush. Default: 100 */
48
+ /** Max records buffered before forced flush. Default: 100 */
36
49
  maxBufferSize?: number;
37
- /** Max events per HTTP request. Default: 50 */
50
+ /** Max records per HTTP request. Default: 50 */
38
51
  maxChunkSize?: number;
39
52
  /** Max bytes per HTTP request. Default: 524288 (512KB) */
40
53
  maxChunkBytes?: number;
41
- /** Persist pending logs (e.g. to AsyncStorage). Called with JSON string. */
54
+ /** Persist pending records (e.g. to AsyncStorage). Called with JSON string. */
42
55
  persistLogs?: (data: string) => Promise<void>;
43
- /** Load previously persisted logs. Returns JSON string or null. */
56
+ /** Load previously persisted records. Returns JSON string or null. */
44
57
  loadPersistedLogs?: () => Promise<string | null>;
45
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redthreadlabs/tracelog-client",
3
- "version": "1.7.0",
3
+ "version": "2.0.0",
4
4
  "description": "Lightweight logging client for tracelog — works in React Native and browsers",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -21,6 +21,6 @@
21
21
  "typescript": "latest"
22
22
  },
23
23
  "dependencies": {
24
- "@redthreadlabs/tracelog-schema": "^0.3.0"
24
+ "@redthreadlabs/tracelog-schema": "^0.4.0"
25
25
  }
26
26
  }