@redthreadlabs/tracelog-client 1.8.0 → 2.1.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,28 +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;
16
- /**
17
- * Record a complete perf measurement in one call — for operations timed
18
- * elsewhere (a pre-measured duration with no live start/end pair). Buffers a
19
- * parent-less, root perf, which the server maps to a `transaction` record.
20
- * Reach for startPerf/endPerf instead when you need a live span tree.
21
- */
22
- recordPerf(name: string, durationMs: number, context?: Record<string, JsonValue>, outcome?: 'success' | 'failure' | 'unknown'): 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;
23
33
  flush(): Promise<void>;
24
34
  dispose(): void;
25
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;
26
41
  private _sendInChunks;
27
42
  private _sendChunkWithRetry;
28
43
  private _sendChunk;
@@ -30,3 +45,27 @@ export declare class LogClient {
30
45
  private _persistNow;
31
46
  private _loadPersistedLogs;
32
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,115 +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);
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);
57
71
  }
58
- endPerf(token, context) {
59
- const active = this._activePerfs.get(token.id);
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
  }
95
- /**
96
- * Record a complete perf measurement in one call for operations timed
97
- * elsewhere (a pre-measured duration with no live start/end pair). Buffers a
98
- * parent-less, root perf, which the server maps to a `transaction` record.
99
- * Reach for startPerf/endPerf instead when you need a live span tree.
100
- */
101
- recordPerf(name, durationMs, context, outcome = 'success') {
102
- if (this._disposed)
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))
103
113
  return;
104
- if (!isFinite(durationMs) || durationMs < 0)
105
- durationMs = 0;
106
114
  const id = randomHex(16);
107
- const end = now();
108
- const perf = {
109
- id,
110
- trace_id: randomHex(32),
111
- root_id: id,
112
- name,
113
- type: 'client-perf',
114
- // No live start time, so back-compute it from the measured duration.
115
- timestamp: Math.round(end - durationMs),
116
- duration: Math.round(durationMs),
117
- outcome,
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',
118
132
  tz_offset: (0, util_1.tzOffsetMinutes)(),
119
133
  };
120
- if (context && Object.keys(context).length > 0) {
121
- perf.context = { tags: context };
122
- }
123
- this._perfBuffer.push(perf);
124
- this._schedulePersist();
125
- if (this._perfBuffer.length + this._eventBuffer.length >= (this._opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE)) {
126
- this.flush();
127
- }
134
+ if (opts?.labels && Object.keys(opts.labels).length > 0)
135
+ rec.context = { labels: opts.labels };
136
+ return rec;
128
137
  }
129
138
  // ---- Transport ----
130
139
  async flush() {
131
140
  if (this._disposed || this._flushing)
132
141
  return;
133
- if (this._eventBuffer.length === 0 && this._perfBuffer.length === 0)
142
+ if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0)
134
143
  return;
135
144
  this._flushing = true;
136
145
  try {
137
146
  const events = this._eventBuffer.splice(0);
138
- const perfs = this._perfBuffer.splice(0);
139
- 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);
140
151
  }
141
152
  finally {
142
153
  this._flushing = false;
@@ -155,109 +166,138 @@ class LogClient {
155
166
  clearTimeout(this._persistHandle);
156
167
  this._persistHandle = null;
157
168
  }
158
- // Persist anything remaining
159
169
  this._persistNow();
160
170
  }
161
- // ---- Internal: event buffering ----
171
+ // ---- Internal: buffering ----
162
172
  _enqueueEvent(event) {
163
173
  if (this._disposed)
164
174
  return;
165
- // Level gate: drop events below the host-supplied minimum before they ever
166
- // buffer, persist, or ship. No callback ⇒ emit everything (back-compat).
175
+ // Level gate: drop events below the host-supplied minimum before buffering.
167
176
  const minLevel = this._opts.getMinLevel?.();
168
177
  if (minLevel && LEVEL_RANK[event.level] < LEVEL_RANK[minLevel])
169
178
  return;
179
+ if (event.locale === undefined) {
180
+ const locale = this._opts.getLocale?.();
181
+ if (locale)
182
+ event.locale = locale;
183
+ }
170
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() {
171
200
  this._schedulePersist();
172
- 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))
173
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;
174
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 };
175
219
  }
176
220
  // ---- Internal: chunked sending ----
177
- async _sendInChunks(events, perfs) {
221
+ async _sendInChunks(events, transactions, spans, origin) {
178
222
  const maxChunkSize = this._opts.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;
179
223
  const maxChunkBytes = this._opts.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
180
- // Combine events and perfs into chunks that respect size limits
181
- let eventIdx = 0;
182
- let perfIdx = 0;
183
- let isFirstChunk = true;
184
- while (eventIdx < events.length || perfIdx < perfs.length) {
185
- 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)
186
233
  await delay(INTER_CHUNK_DELAY_MS);
187
- }
188
- isFirstChunk = false;
189
- const chunkEvents = [];
190
- const chunkPerfs = [];
191
- let estimatedBytes = 200; // base overhead for batch envelope
192
- // Fill chunk with events
193
- while (eventIdx < events.length && chunkEvents.length + chunkPerfs.length < maxChunkSize) {
194
- const itemBytes = estimateJsonSize(events[eventIdx]);
195
- if (estimatedBytes + itemBytes > maxChunkBytes && chunkEvents.length > 0)
196
- break;
197
- chunkEvents.push(events[eventIdx]);
198
- estimatedBytes += itemBytes;
199
- eventIdx++;
200
- }
201
- // Fill chunk with perfs
202
- while (perfIdx < perfs.length && chunkEvents.length + chunkPerfs.length < maxChunkSize) {
203
- const itemBytes = estimateJsonSize(perfs[perfIdx]);
204
- 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)
205
242
  break;
206
- chunkPerfs.push(perfs[perfIdx]);
207
- estimatedBytes += itemBytes;
208
- 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++;
209
251
  }
210
- 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)
211
255
  break;
212
- await this._sendChunkWithRetry(chunkEvents, chunkPerfs);
256
+ await this._sendChunkWithRetry(ce, ct, cs, chunkOrigin);
213
257
  }
214
258
  }
215
- async _sendChunkWithRetry(events, perfs) {
259
+ async _sendChunkWithRetry(events, transactions, spans, origin) {
216
260
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
217
261
  try {
218
- await this._sendChunk(events, perfs);
262
+ await this._sendChunk(events, transactions, spans, origin);
219
263
  return;
220
264
  }
221
- catch (err) {
265
+ catch {
222
266
  if (attempt < MAX_RETRIES) {
223
267
  await delay(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
224
268
  }
225
269
  else {
226
- // 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.
227
272
  this._eventBuffer.push(...events);
228
- this._perfBuffer.push(...perfs);
273
+ this._transactionBuffer.push(...transactions);
274
+ this._spanBuffer.push(...spans);
275
+ if (origin)
276
+ this._lastOriginJson = null;
229
277
  this._schedulePersist();
230
278
  }
231
279
  }
232
280
  }
233
281
  }
234
- async _sendChunk(events, perfs) {
235
- const batch = {
236
- client: this._opts.client,
237
- events,
238
- perfs,
239
- };
282
+ async _sendChunk(events, transactions, spans, origin) {
283
+ const batch = { events, transactions, spans };
284
+ batch.lifetime_id = this._lifetimeId;
240
285
  const userId = this._opts.getUserId?.();
241
- const sessionRef = this._opts.getSessionRef?.();
242
286
  const deviceId = this._opts.getDeviceId?.();
243
287
  if (userId)
244
288
  batch.user_id = userId;
245
- if (sessionRef)
246
- batch.session_ref = sessionRef;
247
289
  if (deviceId)
248
290
  batch.device_id = deviceId;
291
+ if (origin)
292
+ batch.origin = origin;
249
293
  const headers = await Promise.resolve(this._opts.getAuthHeaders());
250
294
  const response = await fetch(this._opts.endpoint, {
251
295
  method: 'POST',
252
- headers: {
253
- 'Content-Type': 'application/json',
254
- ...headers,
255
- },
296
+ headers: { 'Content-Type': 'application/json', ...headers },
256
297
  body: JSON.stringify(batch),
257
298
  });
258
- if (!response.ok) {
299
+ if (!response.ok)
259
300
  throw new Error(`Server returned ${response.status}`);
260
- }
261
301
  }
262
302
  // ---- Internal: persistence ----
263
303
  _schedulePersist() {
@@ -271,13 +311,14 @@ class LogClient {
271
311
  _persistNow() {
272
312
  if (!this._opts.persistLogs)
273
313
  return;
274
- if (this._eventBuffer.length === 0 && this._perfBuffer.length === 0) {
314
+ if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0) {
275
315
  this._opts.persistLogs('').catch(() => { });
276
316
  return;
277
317
  }
278
318
  const data = JSON.stringify({
279
319
  events: this._eventBuffer,
280
- perfs: this._perfBuffer,
320
+ transactions: this._transactionBuffer,
321
+ spans: this._spanBuffer,
281
322
  });
282
323
  this._opts.persistLogs(data).catch(() => { });
283
324
  }
@@ -289,13 +330,12 @@ class LogClient {
289
330
  if (!data)
290
331
  return;
291
332
  const parsed = JSON.parse(data);
292
- if (Array.isArray(parsed.events)) {
333
+ if (Array.isArray(parsed.events))
293
334
  this._eventBuffer.push(...parsed.events);
294
- }
295
- if (Array.isArray(parsed.perfs)) {
296
- this._perfBuffer.push(...parsed.perfs);
297
- }
298
- // 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);
299
339
  this._opts.persistLogs?.('').catch(() => { });
300
340
  }
301
341
  catch {
@@ -304,6 +344,35 @@ class LogClient {
304
344
  }
305
345
  }
306
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;
307
376
  // ---- Helpers ----
308
377
  function randomHex(length) {
309
378
  const chars = '0123456789abcdef';
@@ -315,8 +384,6 @@ function randomHex(length) {
315
384
  }
316
385
  function now() {
317
386
  if (typeof performance !== 'undefined' && performance.now) {
318
- // Use performance.now() for high-res timing, but we need wall-clock for timestamps
319
- // Store the offset on first call
320
387
  if (!now._offset) {
321
388
  now._offset = Date.now() - performance.now();
322
389
  }
@@ -328,6 +395,5 @@ function delay(ms) {
328
395
  return new Promise(resolve => setTimeout(resolve, ms));
329
396
  }
330
397
  function estimateJsonSize(obj) {
331
- // Fast estimate — avoid full serialization during chunking
332
398
  return JSON.stringify(obj).length;
333
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,396 @@
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
+ if (userId)
287
+ batch.user_id = userId;
288
+ if (origin)
289
+ batch.origin = origin;
290
+ const headers = await Promise.resolve(this._opts.getAuthHeaders());
291
+ const response = await fetch(this._opts.endpoint, {
292
+ method: 'POST',
293
+ headers: { 'Content-Type': 'application/json', ...headers },
294
+ body: JSON.stringify(batch),
295
+ });
296
+ if (!response.ok)
297
+ throw new Error(`Server returned ${response.status}`);
298
+ }
299
+ // ---- Internal: persistence ----
300
+ _schedulePersist() {
301
+ if (this._persistHandle || !this._opts.persistLogs)
302
+ return;
303
+ this._persistHandle = setTimeout(() => {
304
+ this._persistHandle = null;
305
+ this._persistNow();
306
+ }, PERSIST_DEBOUNCE_MS);
307
+ }
308
+ _persistNow() {
309
+ if (!this._opts.persistLogs)
310
+ return;
311
+ if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0) {
312
+ this._opts.persistLogs('').catch(() => { });
313
+ return;
314
+ }
315
+ const data = JSON.stringify({
316
+ events: this._eventBuffer,
317
+ transactions: this._transactionBuffer,
318
+ spans: this._spanBuffer,
319
+ });
320
+ this._opts.persistLogs(data).catch(() => { });
321
+ }
322
+ async _loadPersistedLogs() {
323
+ if (!this._opts.loadPersistedLogs)
324
+ return;
325
+ try {
326
+ const data = await this._opts.loadPersistedLogs();
327
+ if (!data)
328
+ return;
329
+ const parsed = JSON.parse(data);
330
+ if (Array.isArray(parsed.events))
331
+ this._eventBuffer.push(...parsed.events);
332
+ if (Array.isArray(parsed.transactions))
333
+ this._transactionBuffer.push(...parsed.transactions);
334
+ if (Array.isArray(parsed.spans))
335
+ this._spanBuffer.push(...parsed.spans);
336
+ this._opts.persistLogs?.('').catch(() => { });
337
+ }
338
+ catch {
339
+ // Ignore parse errors from corrupted persisted data
340
+ }
341
+ }
342
+ }
343
+ exports.TracelogClient = TracelogClient;
344
+ // ---- Trace handles ----
345
+ /** A live root operation. Recorded as a `transaction` when ended. */
346
+ class Transaction {
347
+ constructor(_client, id, traceId, type) {
348
+ this._client = _client;
349
+ this.id = id;
350
+ this.traceId = traceId;
351
+ this.type = type;
352
+ }
353
+ /** A transaction is its own trace root. */
354
+ get transactionId() { return this.id; }
355
+ startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
356
+ end(opts) { this._client._end(this.id, opts); }
357
+ }
358
+ exports.Transaction = Transaction;
359
+ /** A live sub-operation. Recorded as a `span` when ended. */
360
+ class Span {
361
+ constructor(_client, id, traceId, transactionId, parentId, type) {
362
+ this._client = _client;
363
+ this.id = id;
364
+ this.traceId = traceId;
365
+ this.transactionId = transactionId;
366
+ this.parentId = parentId;
367
+ this.type = type;
368
+ }
369
+ startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
370
+ end(opts) { this._client._end(this.id, opts); }
371
+ }
372
+ exports.Span = Span;
373
+ // ---- Helpers ----
374
+ function randomHex(length) {
375
+ const chars = '0123456789abcdef';
376
+ let result = '';
377
+ for (let i = 0; i < length; i++) {
378
+ result += chars[Math.floor(Math.random() * 16)];
379
+ }
380
+ return result;
381
+ }
382
+ function now() {
383
+ if (typeof performance !== 'undefined' && performance.now) {
384
+ if (!now._offset) {
385
+ now._offset = Date.now() - performance.now();
386
+ }
387
+ return now._offset + performance.now();
388
+ }
389
+ return Date.now();
390
+ }
391
+ function delay(ms) {
392
+ return new Promise(resolve => setTimeout(resolve, ms));
393
+ }
394
+ function estimateJsonSize(obj) {
395
+ return JSON.stringify(obj).length;
396
+ }
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,56 @@
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 */
25
- getDeviceId?: () => string | undefined;
35
+ /** Returns the UI locale at event time; stamped onto each event. */
36
+ getLocale?: () => string | undefined;
26
37
  /**
27
38
  * Returns the minimum level to emit. Events whose level ranks below this
28
39
  * (debug < info < warn < error) are dropped before buffering — they never
29
40
  * persist or ship. Called once per event, so keep it cheap and synchronous.
30
- * Omit to emit every level (default). Perfs are unaffected.
41
+ * Omit to emit every level (default). Transactions/spans are unaffected.
31
42
  */
32
43
  getMinLevel?: () => LogLevel;
33
44
  /** Flush cadence in ms. Default: 5000 */
34
45
  flushCadenceMs?: number;
35
- /** Max events buffered before forced flush. Default: 100 */
46
+ /** Max records buffered before forced flush. Default: 100 */
36
47
  maxBufferSize?: number;
37
- /** Max events per HTTP request. Default: 50 */
48
+ /** Max records per HTTP request. Default: 50 */
38
49
  maxChunkSize?: number;
39
50
  /** Max bytes per HTTP request. Default: 524288 (512KB) */
40
51
  maxChunkBytes?: number;
41
- /** Persist pending logs (e.g. to AsyncStorage). Called with JSON string. */
52
+ /** Persist pending records (e.g. to AsyncStorage). Called with JSON string. */
42
53
  persistLogs?: (data: string) => Promise<void>;
43
- /** Load previously persisted logs. Returns JSON string or null. */
54
+ /** Load previously persisted records. Returns JSON string or null. */
44
55
  loadPersistedLogs?: () => Promise<string | null>;
45
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redthreadlabs/tracelog-client",
3
- "version": "1.8.0",
3
+ "version": "2.1.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.5.0"
25
25
  }
26
26
  }