sdk-garden-js 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -26,26 +26,70 @@ module.exports = __toCommonJS(index_exports);
26
26
  var _config = null;
27
27
  var _queue = [];
28
28
  var _sessionId = "";
29
+ var _customer = null;
29
30
  var _flushTimer = null;
30
- var _unloadHandler = null;
31
- var DEFAULT_ENDPOINT = "https://ingest.sdkgarden.dev/track";
31
+ var _exiting = false;
32
+ var _exitDrains = 0;
33
+ var _exitStartedAt = 0;
34
+ var _exitActiveAt = 0;
35
+ var _exitSendFailed = false;
36
+ var _exitTurn = false;
37
+ var _exitTurnCount = 0;
38
+ var _exitProbe = null;
39
+ var _exitProbeDueAt = Infinity;
40
+ var _pending = /* @__PURE__ */ new Set();
41
+ var _registered = /* @__PURE__ */ new Set();
42
+ var _undeclared = [];
43
+ var _declareScheduled = false;
44
+ var DEFAULT_ENDPOINT = "https://ingest.sdkgarden.com/track";
45
+ var MAX_BATCH_SIZE = 100;
46
+ var MAX_QUEUE_SIZE = 1e3;
47
+ var MAX_DRAIN_REQUESTS = 10;
48
+ var MAX_EVENT_BYTES = 64 * 1024;
49
+ var MAX_REQUEST_BYTES = 512 * 1024;
50
+ var MAX_ERROR_MESSAGE_BYTES = 1e3;
51
+ var MAX_EMAIL_LENGTH = 254;
52
+ var MAX_EVENT_NAME_LENGTH = 100;
53
+ var MAX_CUSTOMER_ID_LENGTH = 128;
54
+ var MAX_CUSTOMER_NAME_LENGTH = 200;
55
+ var MAX_EXIT_DRAINS = 3;
56
+ var NEXT_BATCH_DELAY_MS = 10;
57
+ var EXIT_PROBE_MS = 100;
58
+ var EXIT_FROZEN_MS = 250;
59
+ var EXIT_QUIET_MS = 1e3;
60
+ var KEEPALIVE_BUDGET_BYTES = 32 * 1024;
61
+ var WAIT_TIMEOUT_MS = 2e3;
62
+ var MAX_TIMER_MS = 2147483647;
32
63
  var MODULE_LOADED_AT = Date.now();
64
+ var MAX_SURFACE_METHODS = 500;
65
+ var MAX_METHOD_NAME_LENGTH = 255;
66
+ var MAX_SURFACE_CHARS = 32 * 1024;
67
+ function _clamp(value, min, max, fallback) {
68
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
69
+ return Math.min(max, Math.max(min, Math.floor(value)));
70
+ }
33
71
  function _mergeDefaults(config) {
34
- var _a, _b, _c, _d, _e;
72
+ var _a, _b;
35
73
  return {
36
74
  apiKey: config.apiKey,
37
75
  sdkName: config.sdkName,
38
76
  sdkVersion: config.sdkVersion,
39
- endpoint: (_a = config.endpoint) != null ? _a : DEFAULT_ENDPOINT,
40
- debug: (_b = config.debug) != null ? _b : false,
41
- disabled: (_c = config.disabled) != null ? _c : false,
42
- batchSize: (_d = config.batchSize) != null ? _d : 10,
43
- flushInterval: (_e = config.flushInterval) != null ? _e : 5e3
77
+ // An empty endpoint would POST to the host page's own URL.
78
+ endpoint: typeof config.endpoint === "string" && config.endpoint !== "" ? config.endpoint : DEFAULT_ENDPOINT,
79
+ debug: (_a = config.debug) != null ? _a : false,
80
+ disabled: (_b = config.disabled) != null ? _b : false,
81
+ // A batch size below 1 would never empty the queue; above 100 the endpoint rejects it.
82
+ batchSize: _clamp(config.batchSize, 1, MAX_BATCH_SIZE, 10),
83
+ // A tiny interval would send constantly; one too large for a timer would fire every 1 ms.
84
+ flushInterval: _clamp(config.flushInterval, 100, MAX_TIMER_MS, 5e3)
44
85
  };
45
86
  }
46
87
  function _generateSessionId() {
47
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
48
- return crypto.randomUUID();
88
+ try {
89
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
90
+ return crypto.randomUUID();
91
+ }
92
+ } catch (e) {
49
93
  }
50
94
  return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
51
95
  }
@@ -79,11 +123,67 @@ function _getMetadata() {
79
123
  }
80
124
  return meta;
81
125
  }
82
- function _scrubPII(str) {
83
- if (typeof str !== "string") return str;
84
- return str.replace(
85
- /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
86
- "[REDACTED]"
126
+ var EMAIL_PATTERN = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
127
+ function _customerField(value, maxLength) {
128
+ if (typeof value !== "string") return "";
129
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").trim().slice(0, maxLength).trim();
130
+ }
131
+ function _cleanCustomer(customer) {
132
+ if (customer === null || typeof customer !== "object") return null;
133
+ const c = customer;
134
+ const id = _customerField(c["id"], MAX_CUSTOMER_ID_LENGTH);
135
+ if (id === "") {
136
+ _log("Ignored a customer without an id");
137
+ return null;
138
+ }
139
+ if (EMAIL_PATTERN.test(id)) {
140
+ _log("Ignored a customer id that contains an email address");
141
+ return null;
142
+ }
143
+ const name = _customerField(c["name"], MAX_CUSTOMER_NAME_LENGTH);
144
+ if (name !== "" && EMAIL_PATTERN.test(name)) {
145
+ _log("Left out a customer name that contains an email address");
146
+ return { id };
147
+ }
148
+ return name !== "" ? { id, name } : { id };
149
+ }
150
+ function _truncateUtf8(str, maxBytes) {
151
+ let bytes = 0;
152
+ for (let i = 0; i < str.length; i++) {
153
+ const code = str.charCodeAt(i);
154
+ let size = code < 128 ? 1 : code < 2048 ? 2 : 3;
155
+ if (code >= 55296 && code <= 56319 && i + 1 < str.length) {
156
+ const next = str.charCodeAt(i + 1);
157
+ if (next >= 56320 && next <= 57343) size = 4;
158
+ }
159
+ if (bytes + size > maxBytes) return str.slice(0, i);
160
+ bytes += size;
161
+ if (size === 4) i++;
162
+ }
163
+ return str;
164
+ }
165
+ function _errorMessage(error) {
166
+ const message = error == null ? void 0 : error.message;
167
+ if (typeof message !== "string") return message;
168
+ const searched = message.slice(0, MAX_ERROR_MESSAGE_BYTES + MAX_EMAIL_LENGTH);
169
+ const pattern = new RegExp(EMAIL_PATTERN.source, "g");
170
+ let scrubbed = "";
171
+ let end = 0;
172
+ let match = pattern.exec(searched);
173
+ while (match && match.index < MAX_ERROR_MESSAGE_BYTES) {
174
+ scrubbed += searched.slice(end, match.index) + "[REDACTED]";
175
+ end = match.index + match[0].length;
176
+ match = pattern.exec(searched);
177
+ }
178
+ if (end < MAX_ERROR_MESSAGE_BYTES) scrubbed += searched.slice(end, MAX_ERROR_MESSAGE_BYTES);
179
+ return _truncateUtf8(scrubbed.replace(/\u0000/g, "\uFFFD"), MAX_ERROR_MESSAGE_BYTES);
180
+ }
181
+ var JSON_ESCAPE = /\\(?:u(0000|d[89ab][0-9a-f]{2}(?:\\ud[c-f][0-9a-f]{2})?|d[c-f][0-9a-f]{2})|[\s\S])/g;
182
+ function _sanitizeJson(json) {
183
+ if (json.indexOf("\\u") === -1) return json;
184
+ return json.replace(
185
+ JSON_ESCAPE,
186
+ (escape, code) => code !== void 0 && code.length === 4 ? "\\ufffd" : escape
87
187
  );
88
188
  }
89
189
  function _sanitizeConfig(config) {
@@ -97,109 +197,520 @@ function _sanitizeConfig(config) {
97
197
  return sanitized;
98
198
  }
99
199
  function _log(...args) {
100
- if (_config == null ? void 0 : _config.debug) {
200
+ if (!(_config == null ? void 0 : _config.debug)) return;
201
+ try {
101
202
  console.log("[SDK Garden]", ...args);
203
+ } catch (e) {
204
+ }
205
+ }
206
+ function _now() {
207
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
208
+ }
209
+ function _unref(timer) {
210
+ try {
211
+ if (timer !== null && typeof timer === "object" && typeof timer.unref === "function") {
212
+ timer.unref();
213
+ return true;
214
+ } else if (typeof timer === "number" && typeof Deno !== "undefined" && typeof (Deno == null ? void 0 : Deno.unrefTimer) === "function") {
215
+ Deno.unrefTimer(timer);
216
+ return true;
217
+ }
218
+ } catch (e) {
219
+ }
220
+ return false;
221
+ }
222
+ function _requestTimeoutMs() {
223
+ return typeof window === "undefined" ? 2e3 : 1e4;
224
+ }
225
+ function _utf8Length(str) {
226
+ let bytes = str.length;
227
+ for (let i = 0; i < str.length; i++) {
228
+ const code = str.charCodeAt(i);
229
+ if (code > 127) bytes += code > 2047 ? 2 : 1;
102
230
  }
231
+ return bytes;
103
232
  }
104
233
  function _enqueue(event) {
105
- _queue.push(event);
234
+ _checkExit();
235
+ if (typeof event.event !== "string" || event.event.length > MAX_EVENT_NAME_LENGTH) {
236
+ _log("Dropped an event whose name is not a string of at most 100 characters");
237
+ return;
238
+ }
239
+ if (_queue.length >= MAX_QUEUE_SIZE) {
240
+ _log("Queue full; dropped event", event.event);
241
+ return;
242
+ }
243
+ if (_customer) event.customer = _customer;
244
+ let json;
245
+ try {
246
+ json = JSON.stringify(event);
247
+ } catch (e) {
248
+ _log("Dropped an event that cannot be serialized", event.event);
249
+ return;
250
+ }
251
+ if (json.length <= MAX_EVENT_BYTES) json = _sanitizeJson(json);
252
+ const bytes = _utf8Length(json);
253
+ if (bytes > MAX_EVENT_BYTES) {
254
+ _log("Dropped an event larger than 64 KiB", event.event);
255
+ return;
256
+ }
257
+ _queue.push({ json, bytes });
106
258
  _log("Event tracked", event.event);
107
- if (_config && _queue.length >= _config.batchSize) {
108
- _flush();
259
+ if (_config && _queue.length >= _config.batchSize && !_exiting) {
260
+ _pump();
261
+ }
262
+ }
263
+ function _safely(fn) {
264
+ try {
265
+ fn();
266
+ } catch (e) {
267
+ }
268
+ }
269
+ function _trackSuccess(methodName, startTime, isAsync, argsCount) {
270
+ _safely(
271
+ () => SdkGarden.track("method_success", {
272
+ method: methodName,
273
+ duration_ms: Math.round(_now() - startTime),
274
+ is_async: isAsync,
275
+ args_count: argsCount
276
+ })
277
+ );
278
+ }
279
+ function _trackFailure(methodName, error, startTime, isAsync) {
280
+ _safely(() => {
281
+ const err = error instanceof Error ? error : new Error(String(error));
282
+ SdkGarden.trackError(err, {
283
+ method: methodName,
284
+ duration_ms: Math.round(_now() - startTime),
285
+ is_async: isAsync
286
+ });
287
+ });
288
+ }
289
+ var NATIVE_PROMISE_PROTOTYPE = Object.getPrototypeOf((async () => {
290
+ })());
291
+ function _watchPromise(value, onFulfilled, onRejected) {
292
+ try {
293
+ if (value === null || typeof value !== "object") return false;
294
+ const proto = Object.getPrototypeOf(value);
295
+ if (proto === NATIVE_PROMISE_PROTOTYPE || proto === Promise.prototype) {
296
+ if (value.then !== proto.then || value.constructor !== proto.constructor) return false;
297
+ value.then(onFulfilled, onRejected);
298
+ return true;
299
+ }
300
+ if (Object.getPrototypeOf(Object.getPrototypeOf(proto)) !== null || value.constructor !== proto.constructor || Object.prototype.toString.call(value) !== "[object Promise]") {
301
+ return false;
302
+ }
303
+ NATIVE_PROMISE_PROTOTYPE.then.call(value, onFulfilled, onRejected);
304
+ return true;
305
+ } catch (e) {
306
+ return false;
109
307
  }
110
308
  }
111
309
  function _createWrapper(methodName, fn) {
112
- return function(...args) {
113
- if (!_config || _config.disabled) return fn.call(this, ...args);
114
- const startTime = typeof performance !== "undefined" ? performance.now() : Date.now();
115
- SdkGarden.track("method_called", { method: methodName, args_count: args.length });
116
- let result;
310
+ if (typeof fn !== "function" || typeof Proxy !== "function") return fn;
311
+ const wrapped = new Proxy(fn, {
312
+ apply(target, thisArg, args) {
313
+ if (!_config) return Reflect.apply(target, thisArg, args);
314
+ const startTime = _now();
315
+ _safely(() => SdkGarden.track("method_called", { method: methodName, args_count: args.length }));
316
+ let result;
317
+ try {
318
+ result = Reflect.apply(target, thisArg, args);
319
+ } catch (error) {
320
+ _trackFailure(methodName, error, startTime, false);
321
+ throw error;
322
+ }
323
+ const watched = _watchPromise(
324
+ result,
325
+ () => _trackSuccess(methodName, startTime, true, args.length),
326
+ (error) => _trackFailure(methodName, error, startTime, true)
327
+ );
328
+ if (!watched) _trackSuccess(methodName, startTime, false, args.length);
329
+ return result;
330
+ },
331
+ construct(target, args, newTarget) {
332
+ return Reflect.construct(target, args, newTarget === wrapped ? target : newTarget);
333
+ },
334
+ get(target, key) {
335
+ if (key === Symbol.hasInstance) return (value) => value instanceof target;
336
+ return Reflect.get(target, key);
337
+ }
338
+ });
339
+ return wrapped;
340
+ }
341
+ function _isMethodName(name) {
342
+ return typeof name === "string" && name.trim() !== "" && name.length <= MAX_METHOD_NAME_LENGTH;
343
+ }
344
+ function _surfaceList(names) {
345
+ const out = /* @__PURE__ */ new Set();
346
+ let chars = 0;
347
+ for (const name of names) {
348
+ if (out.size >= MAX_SURFACE_METHODS) break;
349
+ if (!_isMethodName(name) || out.has(name)) continue;
350
+ if (chars + name.length > MAX_SURFACE_CHARS) break;
351
+ out.add(name);
352
+ chars += name.length;
353
+ }
354
+ return Array.from(out);
355
+ }
356
+ function _register(methodName) {
357
+ if (!_isMethodName(methodName) || _registered.has(methodName) || _registered.size >= MAX_SURFACE_METHODS) return;
358
+ _registered.add(methodName);
359
+ if (!_config) return;
360
+ _undeclared.push(methodName);
361
+ if (_declareScheduled) return;
362
+ _declareScheduled = true;
363
+ Promise.resolve().then(
364
+ () => _safely(() => {
365
+ _declareScheduled = false;
366
+ const methods = _surfaceList(_undeclared);
367
+ _undeclared = [];
368
+ if (methods.length > 0) SdkGarden.track("api_surface", { api_surface: methods });
369
+ })
370
+ );
371
+ }
372
+ function _keepaliveBytesInFlight() {
373
+ let total = 0;
374
+ _pending.forEach((request) => {
375
+ total += request.keepaliveBytes;
376
+ });
377
+ return total;
378
+ }
379
+ function _takeBatch(events, maxEvents) {
380
+ let count = 0;
381
+ let bytes = 0;
382
+ while (count < events.length && count < maxEvents && (count === 0 || bytes + events[count].bytes <= MAX_REQUEST_BYTES)) {
383
+ bytes += events[count].bytes + 1;
384
+ count++;
385
+ }
386
+ return events.splice(0, count).map((event) => event.json);
387
+ }
388
+ function _send(config, batch, keepalive) {
389
+ var _a;
390
+ if (typeof fetch !== "function") {
391
+ _log("No global fetch() in this runtime; dropping", batch.length, "events");
392
+ return;
393
+ }
394
+ const body = `{"api_key":${JSON.stringify(String((_a = config.apiKey) != null ? _a : ""))},"events":[${batch.join(",")}]}`;
395
+ let keepaliveBytes = keepalive ? _utf8Length(body) : 0;
396
+ if (keepaliveBytes > 0 && _keepaliveBytesInFlight() + keepaliveBytes > KEEPALIVE_BUDGET_BYTES) {
397
+ keepaliveBytes = 0;
398
+ }
399
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
400
+ let settle = () => {
401
+ };
402
+ let delivered = false;
403
+ const request = {
404
+ done: new Promise((resolve) => {
405
+ settle = resolve;
406
+ }),
407
+ keepaliveBytes,
408
+ abort() {
409
+ try {
410
+ controller == null ? void 0 : controller.abort();
411
+ } catch (e) {
412
+ }
413
+ settle();
414
+ }
415
+ };
416
+ let timer;
417
+ try {
418
+ timer = setTimeout(request.abort, _requestTimeoutMs());
419
+ _unref(timer);
420
+ } catch (e) {
421
+ }
422
+ _pending.add(request);
423
+ request.done.then(
424
+ () => _safely(() => {
425
+ if (timer !== void 0) clearTimeout(timer);
426
+ if (!_pending.delete(request)) return;
427
+ if (_exiting) {
428
+ _exitActiveAt = _now();
429
+ if (!delivered) _exitSendFailed = true;
430
+ } else if (delivered && _config && _queue.length >= _config.batchSize) {
431
+ _pumpSoon();
432
+ }
433
+ })
434
+ );
435
+ _log("Sending events", batch.length);
436
+ try {
437
+ fetch(config.endpoint, {
438
+ method: "POST",
439
+ headers: { "Content-Type": "application/json" },
440
+ body,
441
+ keepalive: keepaliveBytes > 0,
442
+ signal: controller == null ? void 0 : controller.signal
443
+ }).then(
444
+ (response) => {
445
+ let discarded;
446
+ try {
447
+ delivered = Boolean(response && response.ok);
448
+ const stream = response == null ? void 0 : response.body;
449
+ if (stream && typeof stream.cancel === "function") discarded = stream.cancel();
450
+ } catch (e) {
451
+ }
452
+ Promise.resolve(discarded).then(settle, settle);
453
+ },
454
+ () => settle()
455
+ // Silent failure — locked by CLIENT-01 "fail silently on network errors".
456
+ );
457
+ } catch (e) {
458
+ settle();
459
+ }
460
+ }
461
+ function _pump() {
462
+ const config = _config;
463
+ if (!config || _exiting || _pending.size > 0 || _queue.length === 0) return;
464
+ _send(config, _takeBatch(_queue, config.batchSize), false);
465
+ }
466
+ function _pumpSoon() {
467
+ try {
468
+ _unref(setTimeout(_pump, NEXT_BATCH_DELAY_MS));
469
+ } catch (e) {
470
+ _pump();
471
+ }
472
+ }
473
+ function _drainAll(keepalive, all = false) {
474
+ const config = _config;
475
+ if (!config || _queue.length === 0) return;
476
+ if (_exiting && _exitTurn && !all) {
477
+ if (_exitDrains >= MAX_EXIT_DRAINS || _exitSendFailed || _now() - _exitStartedAt >= WAIT_TIMEOUT_MS) {
478
+ return;
479
+ }
480
+ _exitDrains++;
481
+ }
482
+ const events = _queue;
483
+ _queue = [];
484
+ const size = Math.min(
485
+ MAX_BATCH_SIZE,
486
+ Math.max(config.batchSize, Math.ceil(events.length / MAX_DRAIN_REQUESTS))
487
+ );
488
+ for (let i = 0; (all || i < MAX_DRAIN_REQUESTS) && events.length > 0; i++) {
489
+ _send(config, _takeBatch(events, size), keepalive);
490
+ }
491
+ if (events.length > 0) _queue = events.concat(_queue);
492
+ }
493
+ function _waitForPending(timeoutMs, abortLate) {
494
+ const requests = Array.from(_pending);
495
+ const waitMs = timeoutMs > 0 ? Math.min(timeoutMs, _requestTimeoutMs()) : 0;
496
+ if (requests.length === 0 || waitMs === 0) {
497
+ if (abortLate) requests.forEach((request) => request.abort());
498
+ return Promise.resolve();
499
+ }
500
+ return new Promise((resolve) => {
501
+ let timer;
117
502
  try {
118
- result = fn.call(this, ...args);
119
- } catch (error) {
120
- const duration2 = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
121
- const err = error instanceof Error ? error : new Error(String(error));
122
- SdkGarden.trackError(err, { method: methodName, duration_ms: Math.round(duration2), is_async: false });
123
- throw error;
503
+ timer = setTimeout(() => {
504
+ if (abortLate) requests.forEach((request) => request.abort());
505
+ resolve();
506
+ }, waitMs);
507
+ _unref(timer);
508
+ } catch (e) {
124
509
  }
125
- if (result !== null && typeof result === "object" && typeof result.then === "function") {
126
- return result.then((value) => {
127
- const duration2 = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
128
- SdkGarden.track("method_success", {
129
- method: methodName,
130
- duration_ms: Math.round(duration2),
131
- is_async: true,
132
- args_count: args.length
133
- });
134
- return value;
135
- }).catch((error) => {
136
- const duration2 = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
137
- const err = error instanceof Error ? error : new Error(String(error));
138
- SdkGarden.trackError(err, { method: methodName, duration_ms: Math.round(duration2), is_async: true });
139
- throw error;
140
- });
510
+ Promise.all(requests.map((request) => request.done)).then(() => {
511
+ if (timer !== void 0) clearTimeout(timer);
512
+ resolve();
513
+ });
514
+ });
515
+ }
516
+ function _onNextLoopTurn(fn) {
517
+ try {
518
+ _unref(typeof setImmediate === "function" ? setImmediate(fn) : setTimeout(fn, 0));
519
+ } catch (e) {
520
+ }
521
+ }
522
+ function _afterMicrotasks(fn) {
523
+ Promise.resolve().then(() => {
524
+ if (typeof process !== "undefined" && typeof (process == null ? void 0 : process.nextTick) === "function") {
525
+ process.nextTick(() => _safely(fn));
526
+ } else {
527
+ _safely(fn);
141
528
  }
142
- const duration = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
143
- SdkGarden.track("method_success", {
144
- method: methodName,
145
- duration_ms: Math.round(duration),
146
- is_async: false,
147
- args_count: args.length
529
+ });
530
+ }
531
+ function _onBeforeExit() {
532
+ _safely(() => {
533
+ _checkExit();
534
+ if (!_exiting) {
535
+ _exiting = true;
536
+ _exitStartedAt = _now();
537
+ }
538
+ _exitActiveAt = _now();
539
+ _beginExitTurn();
540
+ _watchExit();
541
+ _keepListenerFirst();
542
+ const session = _sessionId;
543
+ _afterMicrotasks(() => {
544
+ if (_sessionId !== session) return;
545
+ _drainAll(false);
546
+ void _waitForPending(WAIT_TIMEOUT_MS, true);
148
547
  });
149
- return result;
150
- };
548
+ });
151
549
  }
152
- function _flush() {
153
- if (!_config || _queue.length === 0) return;
154
- const events = _queue.splice(0, _config.batchSize);
155
- const payload = JSON.stringify({ api_key: _config.apiKey, events });
156
- _log("Flushing events", events.length);
157
- fetch(_config.endpoint, {
158
- method: "POST",
159
- headers: { "Content-Type": "application/json" },
160
- body: payload,
161
- keepalive: true
162
- }).catch(() => {
550
+ function _beginExitTurn() {
551
+ _exitTurn = true;
552
+ const turn = ++_exitTurnCount;
553
+ _onNextLoopTurn(() => {
554
+ if (turn === _exitTurnCount) _exitTurn = false;
163
555
  });
164
556
  }
557
+ function _keepListenerFirst() {
558
+ try {
559
+ const proc = typeof process !== "undefined" ? process : void 0;
560
+ if (!proc || typeof proc.listeners !== "function" || typeof proc.prependListener !== "function" || typeof proc.removeListener !== "function" || proc.listeners("beforeExit")[0] === _onBeforeExit) {
561
+ return;
562
+ }
563
+ proc.removeListener("beforeExit", _onBeforeExit);
564
+ proc.prependListener("beforeExit", _onBeforeExit);
565
+ } catch (e) {
566
+ }
567
+ }
568
+ function _watchExit() {
569
+ if (_exitProbe !== null) clearTimeout(_exitProbe);
570
+ _exitProbe = null;
571
+ _exitProbeDueAt = Infinity;
572
+ try {
573
+ const timer = setTimeout(_onExitProbe, EXIT_PROBE_MS);
574
+ if (!_unref(timer)) {
575
+ clearTimeout(timer);
576
+ return;
577
+ }
578
+ _exitProbe = timer;
579
+ _exitProbeDueAt = _now() + EXIT_PROBE_MS;
580
+ } catch (e) {
581
+ }
582
+ }
583
+ function _onExitProbe() {
584
+ _safely(() => {
585
+ _checkExit();
586
+ if (_exiting) _watchExit();
587
+ else _pump();
588
+ });
589
+ }
590
+ function _checkExit() {
591
+ if (!_exiting) return;
592
+ const now = _now();
593
+ const frozen = now - _exitProbeDueAt >= EXIT_FROZEN_MS;
594
+ const quiet = _pending.size === 0 && now - _exitActiveAt >= EXIT_QUIET_MS;
595
+ if (frozen || quiet) _endExit();
596
+ }
597
+ function _endExit() {
598
+ _exiting = false;
599
+ _exitDrains = 0;
600
+ _exitSendFailed = false;
601
+ _exitTurn = false;
602
+ if (_exitProbe !== null) clearTimeout(_exitProbe);
603
+ _exitProbe = null;
604
+ _exitProbeDueAt = Infinity;
605
+ }
606
+ function _onTick() {
607
+ _checkExit();
608
+ if (!_exiting) _pump();
609
+ }
610
+ function _onPageHide() {
611
+ _drainAll(true);
612
+ }
613
+ function _onVisibilityChange() {
614
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
615
+ _drainAll(true);
616
+ }
617
+ }
618
+ function _addLifecycleListeners() {
619
+ if (typeof process !== "undefined") {
620
+ if (typeof (process == null ? void 0 : process.prependListener) === "function") {
621
+ process.prependListener("beforeExit", _onBeforeExit);
622
+ } else if (typeof (process == null ? void 0 : process.on) === "function") {
623
+ process.on("beforeExit", _onBeforeExit);
624
+ }
625
+ }
626
+ if (typeof window !== "undefined" && typeof (window == null ? void 0 : window.addEventListener) === "function") {
627
+ window.addEventListener("pagehide", _onPageHide);
628
+ }
629
+ if (typeof document !== "undefined" && typeof (document == null ? void 0 : document.addEventListener) === "function") {
630
+ document.addEventListener("visibilitychange", _onVisibilityChange);
631
+ }
632
+ }
633
+ function _removeLifecycleListeners() {
634
+ if (typeof process !== "undefined" && typeof (process == null ? void 0 : process.removeListener) === "function") {
635
+ process.removeListener("beforeExit", _onBeforeExit);
636
+ }
637
+ if (typeof window !== "undefined" && typeof (window == null ? void 0 : window.removeEventListener) === "function") {
638
+ window.removeEventListener("pagehide", _onPageHide);
639
+ }
640
+ if (typeof document !== "undefined" && typeof (document == null ? void 0 : document.removeEventListener) === "function") {
641
+ document.removeEventListener("visibilitychange", _onVisibilityChange);
642
+ }
643
+ }
644
+ function _teardown() {
645
+ if (_flushTimer !== null) {
646
+ clearInterval(_flushTimer);
647
+ _flushTimer = null;
648
+ }
649
+ _removeLifecycleListeners();
650
+ _config = null;
651
+ _queue = [];
652
+ _sessionId = "";
653
+ _customer = null;
654
+ _endExit();
655
+ }
656
+ function _start(config) {
657
+ var _a;
658
+ let carried = [];
659
+ if (_config && !config.disabled) {
660
+ const next = _mergeDefaults(config);
661
+ if (next.apiKey === _config.apiKey && next.endpoint === _config.endpoint) {
662
+ carried = _queue.slice(0, MAX_QUEUE_SIZE - 1);
663
+ } else {
664
+ _drainAll(false, true);
665
+ }
666
+ }
667
+ _teardown();
668
+ if (config.disabled) {
669
+ return;
670
+ }
671
+ const initStartedAt = (_a = config.startedAt) != null ? _a : MODULE_LOADED_AT;
672
+ _config = _mergeDefaults(config);
673
+ _queue = carried;
674
+ _sessionId = _generateSessionId();
675
+ _customer = config.customer === void 0 ? null : _cleanCustomer(config.customer);
676
+ _undeclared = [];
677
+ const apiSurface = _surfaceList([
678
+ ...Array.isArray(config.methods) ? config.methods : [],
679
+ ..._registered
680
+ ]);
681
+ _log("Initialised", _sanitizeConfig(_config));
682
+ _enqueue({
683
+ event: "sdk_initialized",
684
+ sdk: { name: _config.sdkName, version: _config.sdkVersion },
685
+ session_id: _sessionId,
686
+ timestamp: Date.now(),
687
+ properties: {
688
+ environment: _getEnvironment(),
689
+ time_to_init_ms: Math.max(0, Date.now() - initStartedAt),
690
+ ...apiSurface.length > 0 ? { api_surface: apiSurface } : {}
691
+ },
692
+ meta: _getMetadata()
693
+ });
694
+ try {
695
+ _flushTimer = setInterval(_onTick, _config.flushInterval);
696
+ _unref(_flushTimer);
697
+ } catch (e) {
698
+ }
699
+ _addLifecycleListeners();
700
+ }
165
701
  var SdkGarden = {
166
702
  /**
167
703
  * Initialize the SDK Garden client.
168
- * Must be called once before any tracking methods.
169
- * When config.disabled is true, this is a no-op.
704
+ * Must be called once before any events are recorded. Calling it again
705
+ * starts a new session. When config.disabled is true, any running client is
706
+ * stopped (its queued events are dropped) and everything stays a no-op.
707
+ * Never throws: if the runtime refuses what init needs, the client stays off.
170
708
  */
171
709
  init(config) {
172
- var _a;
173
- if (config.disabled) {
174
- return;
175
- }
176
- if (_flushTimer !== null) {
177
- clearInterval(_flushTimer);
178
- _flushTimer = null;
179
- }
180
- const initStartedAt = (_a = config.startedAt) != null ? _a : MODULE_LOADED_AT;
181
- _config = _mergeDefaults(config);
182
- _queue = [];
183
- _sessionId = _generateSessionId();
184
- _log("Initialised", _sanitizeConfig(_config));
185
- _enqueue({
186
- event: "sdk_initialized",
187
- sdk: { name: _config.sdkName, version: _config.sdkVersion },
188
- session_id: _sessionId,
189
- timestamp: Date.now(),
190
- properties: {
191
- environment: _getEnvironment(),
192
- time_to_init_ms: Math.max(0, Date.now() - initStartedAt)
193
- },
194
- meta: _getMetadata()
195
- });
196
- _flushTimer = setInterval(_flush, _config.flushInterval);
197
- if (typeof window !== "undefined") {
198
- if (_unloadHandler) {
199
- window.removeEventListener("beforeunload", _unloadHandler);
200
- }
201
- _unloadHandler = () => _flush();
202
- window.addEventListener("beforeunload", _unloadHandler);
710
+ try {
711
+ _start(config);
712
+ } catch (e) {
713
+ _teardown();
203
714
  }
204
715
  },
205
716
  /**
@@ -207,20 +718,24 @@ var SdkGarden = {
207
718
  * No-op if SDK has not been initialised or is disabled.
208
719
  */
209
720
  track(eventName, properties) {
210
- if (!_config || _config.disabled) return;
211
- _enqueue({
212
- event: eventName,
213
- sdk: { name: _config.sdkName, version: _config.sdkVersion },
214
- session_id: _sessionId,
215
- timestamp: Date.now(),
216
- properties: properties != null ? properties : {},
217
- meta: _getMetadata()
218
- });
721
+ const config = _config;
722
+ if (!config) return;
723
+ _safely(
724
+ () => _enqueue({
725
+ event: eventName,
726
+ sdk: { name: config.sdkName, version: config.sdkVersion },
727
+ session_id: _sessionId,
728
+ timestamp: Date.now(),
729
+ properties: properties != null ? properties : {},
730
+ meta: _getMetadata()
731
+ })
732
+ );
219
733
  },
220
734
  /**
221
735
  * Instrument a method with call, success, and error telemetry.
222
736
  *
223
- * **Two-argument form** — wraps a standalone function:
737
+ * **Two-argument form** — wraps a standalone function. Safe to call before
738
+ * init(), for example at module level; calls are recorded once init() runs:
224
739
  * ```ts
225
740
  * function processPayment(amount: number): Receipt { ... }
226
741
  * export const trackedProcessPayment = SdkGarden.trackMethod('processPayment', processPayment);
@@ -233,17 +748,17 @@ var SdkGarden = {
233
748
  * processPayment(amount: number): Receipt { ... }
234
749
  * }
235
750
  * ```
236
- *
237
- * **Async limitation (v1):** For async functions in the two-argument form,
238
- * `method_success` is recorded only when the caller awaits or chains `.then()`.
239
- * Class method decorators do not have this limitation.
240
751
  */
241
752
  trackMethod(methodName, fn) {
753
+ _safely(() => _register(methodName));
242
754
  if (fn !== void 0) {
243
- if (!_config || _config.disabled) return fn;
244
755
  return _createWrapper(methodName, fn);
245
756
  }
246
- return function(target, _context) {
757
+ return function(target, _context, descriptor) {
758
+ if (descriptor !== null && typeof descriptor === "object") {
759
+ if (typeof descriptor.value !== "function") return descriptor;
760
+ return { ...descriptor, value: _createWrapper(methodName, descriptor.value) };
761
+ }
247
762
  return _createWrapper(methodName, target);
248
763
  };
249
764
  },
@@ -253,42 +768,85 @@ var SdkGarden = {
253
768
  * No-op if SDK is uninitialised or disabled.
254
769
  */
255
770
  trackError(error, context) {
256
- if (!_config || _config.disabled) return;
257
- _enqueue({
258
- event: "sdk_error",
259
- sdk: { name: _config.sdkName, version: _config.sdkVersion },
260
- session_id: _sessionId,
261
- timestamp: Date.now(),
262
- properties: {
263
- error_message: _scrubPII(error.message),
264
- error_type: error.name,
265
- ...context != null ? context : {}
266
- },
267
- meta: _getMetadata()
771
+ const config = _config;
772
+ if (!config) return;
773
+ _safely(
774
+ () => _enqueue({
775
+ event: "sdk_error",
776
+ sdk: { name: config.sdkName, version: config.sdkVersion },
777
+ session_id: _sessionId,
778
+ timestamp: Date.now(),
779
+ properties: {
780
+ error_message: _errorMessage(error),
781
+ error_type: error == null ? void 0 : error.name,
782
+ ...context != null ? context : {}
783
+ },
784
+ meta: _getMetadata()
785
+ })
786
+ );
787
+ },
788
+ /**
789
+ * Attribute the session to one of your customers: an app or account, by an
790
+ * ID from your own system. Every event tracked afterwards carries it, so the
791
+ * dashboard can show which customers are on old versions, still call
792
+ * deprecated methods or hit errors. Pass null to stop. Lasts until the next
793
+ * init() or shutdown(); no-op before init().
794
+ *
795
+ * Never pass an email address or other personal data: an ID containing an
796
+ * email address is ignored.
797
+ *
798
+ * @example
799
+ * SdkGarden.setCustomer({ id: account.id, name: account.displayName });
800
+ */
801
+ setCustomer(customer) {
802
+ if (!_config) return;
803
+ _safely(() => {
804
+ _customer = customer === null ? null : _cleanCustomer(customer);
268
805
  });
269
806
  },
270
807
  /**
271
- * Manually trigger a flush of queued events.
272
- * Useful in beforeunload handlers or at process exit.
808
+ * Send every queued event now and wait for the requests to finish.
809
+ * Useful at the end of a serverless handler or a test.
810
+ */
811
+ flush(timeoutMs = WAIT_TIMEOUT_MS) {
812
+ try {
813
+ _checkExit();
814
+ _drainAll(false);
815
+ return _waitForPending(timeoutMs, false);
816
+ } catch (e) {
817
+ return Promise.resolve();
818
+ }
819
+ },
820
+ /**
821
+ * Send every queued event and stop the client. Call it, and await it, from
822
+ * your SDK's close() or before process.exit(): explicit exits and signals
823
+ * skip the automatic send that runs when Node exits on its own.
273
824
  */
274
- _flush,
825
+ shutdown(timeoutMs = WAIT_TIMEOUT_MS) {
826
+ try {
827
+ if (timeoutMs > 0) _drainAll(typeof window !== "undefined", true);
828
+ _teardown();
829
+ return _waitForPending(timeoutMs, true);
830
+ } catch (e) {
831
+ _safely(_teardown);
832
+ return Promise.resolve();
833
+ }
834
+ },
835
+ _flush() {
836
+ void SdkGarden.flush();
837
+ },
275
838
  /**
276
839
  * Reset all internal state.
277
840
  * FOR UNIT TESTS ONLY — not documented in README.
278
- * Clears queue, config, session ID, and flush timer.
841
+ * Clears queue, config, session ID, flush timer, listeners, registered
842
+ * method names and the record of requests in flight, without sending or cancelling anything.
279
843
  */
280
844
  _reset() {
281
- if (_flushTimer !== null) {
282
- clearInterval(_flushTimer);
283
- _flushTimer = null;
284
- }
285
- if (typeof window !== "undefined" && _unloadHandler) {
286
- window.removeEventListener("beforeunload", _unloadHandler);
287
- _unloadHandler = null;
288
- }
289
- _config = null;
290
- _queue = [];
291
- _sessionId = "";
845
+ _teardown();
846
+ _pending.clear();
847
+ _registered.clear();
848
+ _undeclared = [];
292
849
  }
293
850
  };
294
851
  var index_default = SdkGarden;
852
+ module.exports = Object.defineProperties(module.exports.default, { default: { value: module.exports.default }, __esModule: { value: true } });