sdk-garden-js 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,14 +61,37 @@ Requires Node.js 18+ (for the global `fetch`) or any modern browser.
61
61
  | `batchSize` | `number` | `10` | Maximum events per request, from 1 to 100; a full batch is sent right away |
62
62
  | `flushInterval` | `number` | `5000` | Milliseconds between sends of whatever is queued, at least 100 |
63
63
  | `startedAt` | `number` | module load time | `Date.now()` when your SDK began initializing; reported as `time_to_init_ms` for the dashboard's init timing |
64
+ | `methods` | `string[]` | `[]` | Public methods of your SDK that are not wrapped with `trackMethod()`, so the dashboard's API Surface panel can show them when nobody calls them |
65
+ | `customer` | `{ id: string; name?: string }` | _(none)_ | The app or customer account this session runs for (see [Customers](#customers)) |
66
+ | `advisories` | `boolean` | `true` | Show the version advisories you set on the Releases page as one `console.warn` per process (0.3.0+) |
67
+ | `onAdvisory` | `(advisory) => void` | _(none)_ | Handle an advisory yourself instead: `{ version, severity, message, upgradeTo? }` |
68
+
69
+ ### API surface
70
+
71
+ Every name passed to `trackMethod()` is declared as part of your SDK's API surface, once per session (in the `sdk_initialized` event, or in one `api_surface` event for methods wrapped after `init()`). The dashboard lists those methods next to the ones people call, so the methods nobody uses show up with zero calls. Add methods you don't wrap with the `methods` option. Up to 500 names are declared.
72
+
73
+ ### Customers
74
+
75
+ Tell SDK Garden which of your customers a session belongs to, and the dashboard's Customers page shows who is on an old version, who still calls a deprecated method and who hits errors, so you know who to contact.
76
+
77
+ ```typescript
78
+ // When your SDK knows which account it runs for, for example after resolving its API key:
79
+ SdkGarden.setCustomer({ id: account.id, name: account.displayName });
80
+
81
+ // Or at init, if you already know it:
82
+ SdkGarden.init({ apiKey: 'sgk_...', sdkName: 'my-sdk', sdkVersion: '1.0.0', customer: { id: 'acct_123' } });
83
+ ```
84
+
85
+ Use an ID from your own system, such as an account, project or app ID, and optionally a display name. Nothing is attributed unless you call this. Don't pass email addresses or other personal data: an ID that contains an email address is ignored, and so is a name that contains one. IDs are cut to 128 characters and names to 200. `setCustomer(null)` stops attributing; the customer lasts until the next `init()` or `shutdown()`.
64
86
 
65
87
  ## What gets sent
66
88
 
67
89
  Each event carries your SDK's name and version, an event type, a random session ID created when `init()` runs, and a timestamp. Depending on the event:
68
90
 
69
- - **`sdk_initialized`**: runtime (`node` + Node version, or `browser` + screen/viewport size, pixel ratio, timezone and language) and `time_to_init_ms`.
91
+ - **`sdk_initialized`**: runtime (`node` + Node version, or `browser` + screen/viewport size, pixel ratio, timezone and language), `time_to_init_ms`, and the names of your SDK's methods (see [API surface](#api-surface)).
70
92
  - **`method_called` / `method_success`**: method name, duration, sync/async, and the *number* of arguments.
71
93
  - **`sdk_error`**: error type and message (email addresses are redacted) plus the method and duration.
94
+ - **If you set a customer**, every event carries its ID and name (see [Customers](#customers)).
72
95
  - **In browsers**, every event also carries the page's origin (for example `https://shop.example.com`), never the full URL, path, query string or referrer.
73
96
 
74
97
  Never sent: argument or return values, request or response bodies, cookies or stack traces. Error messages go out as they are apart from email addresses, so anything your SDK puts in an error message, such as a hostname, a URL or an ID, goes with it. Nothing is sent when `disabled: true`.
package/dist/index.d.mts CHANGED
@@ -46,6 +46,50 @@ interface SdkGardenConfig {
46
46
  * Default: when sdk-garden-js was first loaded.
47
47
  */
48
48
  startedAt?: number;
49
+ /**
50
+ * Names of your SDK's public methods that are not wrapped with
51
+ * trackMethod(). Sent once per session with every name passed to
52
+ * trackMethod(), so the dashboard can list the methods nobody calls.
53
+ */
54
+ methods?: string[];
55
+ /**
56
+ * The app or customer account this session runs for, so the dashboard can
57
+ * show which customers are on old versions, call deprecated methods or hit
58
+ * errors. Use an ID from your own system (an account or project ID), never
59
+ * an email address or other personal data. Can also be set later with
60
+ * setCustomer(). Default: none.
61
+ */
62
+ customer?: SdkGardenCustomer;
63
+ /**
64
+ * Show the notices you attach to versions on the SDK Garden Releases page
65
+ * (deprecated, broken, security) to developers still running them, as one
66
+ * console.warn per process. Default: true. Set onAdvisory to handle them
67
+ * yourself instead.
68
+ */
69
+ advisories?: boolean;
70
+ /**
71
+ * Called once per process when the version in use has an advisory, instead
72
+ * of the default console.warn. Use it to route the notice through your
73
+ * SDK's own logger. Exceptions it throws are ignored.
74
+ */
75
+ onAdvisory?: (advisory: SdkGardenAdvisory) => void;
76
+ }
77
+ /** A notice the SDK publisher attached to the running version (see SdkGardenConfig.advisories). */
78
+ interface SdkGardenAdvisory {
79
+ /** The version this process runs, e.g. "1.2.0". */
80
+ version: string;
81
+ severity: 'deprecated' | 'broken' | 'security';
82
+ /** The publisher's message; may be empty. */
83
+ message: string;
84
+ /** The version the publisher recommends, if they named one. */
85
+ upgradeTo?: string;
86
+ }
87
+ /** The app or customer account a session is attributed to (see setCustomer()). */
88
+ interface SdkGardenCustomer {
89
+ /** An opaque ID from your system, up to 128 characters, e.g. "acct_123". */
90
+ id: string;
91
+ /** A display name for the dashboard, up to 200 characters, e.g. "Acme Corp". */
92
+ name?: string;
49
93
  }
50
94
  /**
51
95
  * Class method decorator returned by SdkGarden.trackMethod(name), for Stage 3
@@ -74,6 +118,11 @@ interface SdkGardenObject {
74
118
  /** Wrapper form — pass an existing function, receive the instrumented version. */
75
119
  trackMethod<T extends (...args: any[]) => any>(methodName: string, fn: T): T;
76
120
  trackError(error: Error, context?: Record<string, unknown>): void;
121
+ /**
122
+ * Attribute this session's events from now on to a customer, or pass null
123
+ * to stop. No-op before init().
124
+ */
125
+ setCustomer(customer: SdkGardenCustomer | null): void;
77
126
  /**
78
127
  * Send every queued event now. Resolves once they (and any requests already
79
128
  * in flight) have finished, or after `timeoutMs` (default 2000), whichever
@@ -114,4 +163,4 @@ interface SdkGardenObject {
114
163
  */
115
164
  declare const SdkGarden: SdkGardenObject;
116
165
 
117
- export { type SdkGardenConfig, type SdkMethodDecorator, SdkGarden as default };
166
+ export { type SdkGardenAdvisory, type SdkGardenConfig, type SdkGardenCustomer, type SdkMethodDecorator, SdkGarden as default };
package/dist/index.d.ts CHANGED
@@ -46,6 +46,50 @@ interface SdkGardenConfig {
46
46
  * Default: when sdk-garden-js was first loaded.
47
47
  */
48
48
  startedAt?: number;
49
+ /**
50
+ * Names of your SDK's public methods that are not wrapped with
51
+ * trackMethod(). Sent once per session with every name passed to
52
+ * trackMethod(), so the dashboard can list the methods nobody calls.
53
+ */
54
+ methods?: string[];
55
+ /**
56
+ * The app or customer account this session runs for, so the dashboard can
57
+ * show which customers are on old versions, call deprecated methods or hit
58
+ * errors. Use an ID from your own system (an account or project ID), never
59
+ * an email address or other personal data. Can also be set later with
60
+ * setCustomer(). Default: none.
61
+ */
62
+ customer?: SdkGardenCustomer;
63
+ /**
64
+ * Show the notices you attach to versions on the SDK Garden Releases page
65
+ * (deprecated, broken, security) to developers still running them, as one
66
+ * console.warn per process. Default: true. Set onAdvisory to handle them
67
+ * yourself instead.
68
+ */
69
+ advisories?: boolean;
70
+ /**
71
+ * Called once per process when the version in use has an advisory, instead
72
+ * of the default console.warn. Use it to route the notice through your
73
+ * SDK's own logger. Exceptions it throws are ignored.
74
+ */
75
+ onAdvisory?: (advisory: SdkGardenAdvisory) => void;
76
+ }
77
+ /** A notice the SDK publisher attached to the running version (see SdkGardenConfig.advisories). */
78
+ interface SdkGardenAdvisory {
79
+ /** The version this process runs, e.g. "1.2.0". */
80
+ version: string;
81
+ severity: 'deprecated' | 'broken' | 'security';
82
+ /** The publisher's message; may be empty. */
83
+ message: string;
84
+ /** The version the publisher recommends, if they named one. */
85
+ upgradeTo?: string;
86
+ }
87
+ /** The app or customer account a session is attributed to (see setCustomer()). */
88
+ interface SdkGardenCustomer {
89
+ /** An opaque ID from your system, up to 128 characters, e.g. "acct_123". */
90
+ id: string;
91
+ /** A display name for the dashboard, up to 200 characters, e.g. "Acme Corp". */
92
+ name?: string;
49
93
  }
50
94
  /**
51
95
  * Class method decorator returned by SdkGarden.trackMethod(name), for Stage 3
@@ -74,6 +118,11 @@ interface SdkGardenObject {
74
118
  /** Wrapper form — pass an existing function, receive the instrumented version. */
75
119
  trackMethod<T extends (...args: any[]) => any>(methodName: string, fn: T): T;
76
120
  trackError(error: Error, context?: Record<string, unknown>): void;
121
+ /**
122
+ * Attribute this session's events from now on to a customer, or pass null
123
+ * to stop. No-op before init().
124
+ */
125
+ setCustomer(customer: SdkGardenCustomer | null): void;
77
126
  /**
78
127
  * Send every queued event now. Resolves once they (and any requests already
79
128
  * in flight) have finished, or after `timeoutMs` (default 2000), whichever
@@ -114,4 +163,4 @@ interface SdkGardenObject {
114
163
  */
115
164
  declare const SdkGarden: SdkGardenObject;
116
165
 
117
- export { type SdkGardenConfig, type SdkMethodDecorator, SdkGarden as default };
166
+ export { type SdkGardenAdvisory, type SdkGardenConfig, type SdkGardenCustomer, type SdkMethodDecorator, SdkGarden as default };
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ 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
31
  var _exiting = false;
31
32
  var _exitDrains = 0;
@@ -37,6 +38,11 @@ var _exitTurnCount = 0;
37
38
  var _exitProbe = null;
38
39
  var _exitProbeDueAt = Infinity;
39
40
  var _pending = /* @__PURE__ */ new Set();
41
+ var _registered = /* @__PURE__ */ new Set();
42
+ var _undeclared = [];
43
+ var _declareScheduled = false;
44
+ var _advisoriesShown = /* @__PURE__ */ new Set();
45
+ var ADVISORY_HEADER = "SDK-Garden-Advisory";
40
46
  var DEFAULT_ENDPOINT = "https://ingest.sdkgarden.com/track";
41
47
  var MAX_BATCH_SIZE = 100;
42
48
  var MAX_QUEUE_SIZE = 1e3;
@@ -46,6 +52,8 @@ var MAX_REQUEST_BYTES = 512 * 1024;
46
52
  var MAX_ERROR_MESSAGE_BYTES = 1e3;
47
53
  var MAX_EMAIL_LENGTH = 254;
48
54
  var MAX_EVENT_NAME_LENGTH = 100;
55
+ var MAX_CUSTOMER_ID_LENGTH = 128;
56
+ var MAX_CUSTOMER_NAME_LENGTH = 200;
49
57
  var MAX_EXIT_DRAINS = 3;
50
58
  var NEXT_BATCH_DELAY_MS = 10;
51
59
  var EXIT_PROBE_MS = 100;
@@ -55,12 +63,15 @@ var KEEPALIVE_BUDGET_BYTES = 32 * 1024;
55
63
  var WAIT_TIMEOUT_MS = 2e3;
56
64
  var MAX_TIMER_MS = 2147483647;
57
65
  var MODULE_LOADED_AT = Date.now();
66
+ var MAX_SURFACE_METHODS = 500;
67
+ var MAX_METHOD_NAME_LENGTH = 255;
68
+ var MAX_SURFACE_CHARS = 32 * 1024;
58
69
  function _clamp(value, min, max, fallback) {
59
70
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
60
71
  return Math.min(max, Math.max(min, Math.floor(value)));
61
72
  }
62
73
  function _mergeDefaults(config) {
63
- var _a, _b;
74
+ var _a, _b, _c;
64
75
  return {
65
76
  apiKey: config.apiKey,
66
77
  sdkName: config.sdkName,
@@ -72,7 +83,9 @@ function _mergeDefaults(config) {
72
83
  // A batch size below 1 would never empty the queue; above 100 the endpoint rejects it.
73
84
  batchSize: _clamp(config.batchSize, 1, MAX_BATCH_SIZE, 10),
74
85
  // A tiny interval would send constantly; one too large for a timer would fire every 1 ms.
75
- flushInterval: _clamp(config.flushInterval, 100, MAX_TIMER_MS, 5e3)
86
+ flushInterval: _clamp(config.flushInterval, 100, MAX_TIMER_MS, 5e3),
87
+ advisories: (_c = config.advisories) != null ? _c : true,
88
+ onAdvisory: typeof config.onAdvisory === "function" ? config.onAdvisory : null
76
89
  };
77
90
  }
78
91
  function _generateSessionId() {
@@ -115,6 +128,29 @@ function _getMetadata() {
115
128
  return meta;
116
129
  }
117
130
  var EMAIL_PATTERN = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
131
+ function _customerField(value, maxLength) {
132
+ if (typeof value !== "string") return "";
133
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").trim().slice(0, maxLength).trim();
134
+ }
135
+ function _cleanCustomer(customer) {
136
+ if (customer === null || typeof customer !== "object") return null;
137
+ const c = customer;
138
+ const id = _customerField(c["id"], MAX_CUSTOMER_ID_LENGTH);
139
+ if (id === "") {
140
+ _log("Ignored a customer without an id");
141
+ return null;
142
+ }
143
+ if (EMAIL_PATTERN.test(id)) {
144
+ _log("Ignored a customer id that contains an email address");
145
+ return null;
146
+ }
147
+ const name = _customerField(c["name"], MAX_CUSTOMER_NAME_LENGTH);
148
+ if (name !== "" && EMAIL_PATTERN.test(name)) {
149
+ _log("Left out a customer name that contains an email address");
150
+ return { id };
151
+ }
152
+ return name !== "" ? { id, name } : { id };
153
+ }
118
154
  function _truncateUtf8(str, maxBytes) {
119
155
  let bytes = 0;
120
156
  for (let i = 0; i < str.length; i++) {
@@ -208,6 +244,7 @@ function _enqueue(event) {
208
244
  _log("Queue full; dropped event", event.event);
209
245
  return;
210
246
  }
247
+ if (_customer) event.customer = _customer;
211
248
  let json;
212
249
  try {
213
250
  json = JSON.stringify(event);
@@ -305,6 +342,37 @@ function _createWrapper(methodName, fn) {
305
342
  });
306
343
  return wrapped;
307
344
  }
345
+ function _isMethodName(name) {
346
+ return typeof name === "string" && name.trim() !== "" && name.length <= MAX_METHOD_NAME_LENGTH;
347
+ }
348
+ function _surfaceList(names) {
349
+ const out = /* @__PURE__ */ new Set();
350
+ let chars = 0;
351
+ for (const name of names) {
352
+ if (out.size >= MAX_SURFACE_METHODS) break;
353
+ if (!_isMethodName(name) || out.has(name)) continue;
354
+ if (chars + name.length > MAX_SURFACE_CHARS) break;
355
+ out.add(name);
356
+ chars += name.length;
357
+ }
358
+ return Array.from(out);
359
+ }
360
+ function _register(methodName) {
361
+ if (!_isMethodName(methodName) || _registered.has(methodName) || _registered.size >= MAX_SURFACE_METHODS) return;
362
+ _registered.add(methodName);
363
+ if (!_config) return;
364
+ _undeclared.push(methodName);
365
+ if (_declareScheduled) return;
366
+ _declareScheduled = true;
367
+ Promise.resolve().then(
368
+ () => _safely(() => {
369
+ _declareScheduled = false;
370
+ const methods = _surfaceList(_undeclared);
371
+ _undeclared = [];
372
+ if (methods.length > 0) SdkGarden.track("api_surface", { api_surface: methods });
373
+ })
374
+ );
375
+ }
308
376
  function _keepaliveBytesInFlight() {
309
377
  let total = 0;
310
378
  _pending.forEach((request) => {
@@ -321,6 +389,55 @@ function _takeBatch(events, maxEvents) {
321
389
  }
322
390
  return events.splice(0, count).map((event) => event.json);
323
391
  }
392
+ function _parseAdvisory(raw) {
393
+ var _a;
394
+ const fields = {};
395
+ for (const part of raw.split("&")) {
396
+ const eq = part.indexOf("=");
397
+ if (eq <= 0) continue;
398
+ try {
399
+ fields[decodeURIComponent(part.slice(0, eq))] = decodeURIComponent(part.slice(eq + 1).replace(/\+/g, " "));
400
+ } catch (e) {
401
+ return null;
402
+ }
403
+ }
404
+ const severity = fields["severity"];
405
+ if (!fields["version"] || severity !== "deprecated" && severity !== "broken" && severity !== "security") {
406
+ return null;
407
+ }
408
+ return {
409
+ version: fields["version"],
410
+ severity,
411
+ message: ((_a = fields["message"]) != null ? _a : "").slice(0, 280),
412
+ ...fields["upgrade_to"] ? { upgradeTo: fields["upgrade_to"].slice(0, 50) } : {}
413
+ };
414
+ }
415
+ function _advisoryText(sdkName, a) {
416
+ const what = { deprecated: "is deprecated", broken: "has a known problem", security: "has a security issue" }[a.severity];
417
+ let text = `[${sdkName}] Version ${a.version} ${what}.`;
418
+ if (a.message) text += ` ${a.message}`;
419
+ if (a.upgradeTo) text += ` Upgrade to ${a.upgradeTo}.`;
420
+ return text;
421
+ }
422
+ function _handleAdvisory(config, response) {
423
+ var _a, _b;
424
+ if (!config.advisories) return;
425
+ const raw = (_b = (_a = response == null ? void 0 : response.headers) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, ADVISORY_HEADER);
426
+ if (typeof raw !== "string" || raw === "") return;
427
+ const advisory = _parseAdvisory(raw);
428
+ if (!advisory || _advisoriesShown.has(advisory.version)) return;
429
+ _advisoriesShown.add(advisory.version);
430
+ if (config.onAdvisory) {
431
+ try {
432
+ config.onAdvisory(advisory);
433
+ } catch (e) {
434
+ }
435
+ return;
436
+ }
437
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
438
+ console.warn(_advisoryText(config.sdkName, advisory));
439
+ }
440
+ }
324
441
  function _send(config, batch, keepalive) {
325
442
  var _a;
326
443
  if (typeof fetch !== "function") {
@@ -385,6 +502,7 @@ function _send(config, batch, keepalive) {
385
502
  if (stream && typeof stream.cancel === "function") discarded = stream.cancel();
386
503
  } catch (e) {
387
504
  }
505
+ if (delivered) _safely(() => _handleAdvisory(config, response));
388
506
  Promise.resolve(discarded).then(settle, settle);
389
507
  },
390
508
  () => settle()
@@ -586,6 +704,7 @@ function _teardown() {
586
704
  _config = null;
587
705
  _queue = [];
588
706
  _sessionId = "";
707
+ _customer = null;
589
708
  _endExit();
590
709
  }
591
710
  function _start(config) {
@@ -607,6 +726,12 @@ function _start(config) {
607
726
  _config = _mergeDefaults(config);
608
727
  _queue = carried;
609
728
  _sessionId = _generateSessionId();
729
+ _customer = config.customer === void 0 ? null : _cleanCustomer(config.customer);
730
+ _undeclared = [];
731
+ const apiSurface = _surfaceList([
732
+ ...Array.isArray(config.methods) ? config.methods : [],
733
+ ..._registered
734
+ ]);
610
735
  _log("Initialised", _sanitizeConfig(_config));
611
736
  _enqueue({
612
737
  event: "sdk_initialized",
@@ -615,7 +740,8 @@ function _start(config) {
615
740
  timestamp: Date.now(),
616
741
  properties: {
617
742
  environment: _getEnvironment(),
618
- time_to_init_ms: Math.max(0, Date.now() - initStartedAt)
743
+ time_to_init_ms: Math.max(0, Date.now() - initStartedAt),
744
+ ...apiSurface.length > 0 ? { api_surface: apiSurface } : {}
619
745
  },
620
746
  meta: _getMetadata()
621
747
  });
@@ -678,6 +804,7 @@ var SdkGarden = {
678
804
  * ```
679
805
  */
680
806
  trackMethod(methodName, fn) {
807
+ _safely(() => _register(methodName));
681
808
  if (fn !== void 0) {
682
809
  return _createWrapper(methodName, fn);
683
810
  }
@@ -712,6 +839,25 @@ var SdkGarden = {
712
839
  })
713
840
  );
714
841
  },
842
+ /**
843
+ * Attribute the session to one of your customers: an app or account, by an
844
+ * ID from your own system. Every event tracked afterwards carries it, so the
845
+ * dashboard can show which customers are on old versions, still call
846
+ * deprecated methods or hit errors. Pass null to stop. Lasts until the next
847
+ * init() or shutdown(); no-op before init().
848
+ *
849
+ * Never pass an email address or other personal data: an ID containing an
850
+ * email address is ignored.
851
+ *
852
+ * @example
853
+ * SdkGarden.setCustomer({ id: account.id, name: account.displayName });
854
+ */
855
+ setCustomer(customer) {
856
+ if (!_config) return;
857
+ _safely(() => {
858
+ _customer = customer === null ? null : _cleanCustomer(customer);
859
+ });
860
+ },
715
861
  /**
716
862
  * Send every queued event now and wait for the requests to finish.
717
863
  * Useful at the end of a serverless handler or a test.
@@ -746,12 +892,15 @@ var SdkGarden = {
746
892
  /**
747
893
  * Reset all internal state.
748
894
  * FOR UNIT TESTS ONLY — not documented in README.
749
- * Clears queue, config, session ID, flush timer, listeners and the record
750
- * of requests in flight, without sending or cancelling anything.
895
+ * Clears queue, config, session ID, flush timer, listeners, registered
896
+ * method names and the record of requests in flight, without sending or cancelling anything.
751
897
  */
752
898
  _reset() {
753
899
  _teardown();
754
900
  _pending.clear();
901
+ _registered.clear();
902
+ _undeclared = [];
903
+ _advisoriesShown.clear();
755
904
  }
756
905
  };
757
906
  var index_default = SdkGarden;
package/dist/index.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  var _config = null;
3
3
  var _queue = [];
4
4
  var _sessionId = "";
5
+ var _customer = null;
5
6
  var _flushTimer = null;
6
7
  var _exiting = false;
7
8
  var _exitDrains = 0;
@@ -13,6 +14,11 @@ var _exitTurnCount = 0;
13
14
  var _exitProbe = null;
14
15
  var _exitProbeDueAt = Infinity;
15
16
  var _pending = /* @__PURE__ */ new Set();
17
+ var _registered = /* @__PURE__ */ new Set();
18
+ var _undeclared = [];
19
+ var _declareScheduled = false;
20
+ var _advisoriesShown = /* @__PURE__ */ new Set();
21
+ var ADVISORY_HEADER = "SDK-Garden-Advisory";
16
22
  var DEFAULT_ENDPOINT = "https://ingest.sdkgarden.com/track";
17
23
  var MAX_BATCH_SIZE = 100;
18
24
  var MAX_QUEUE_SIZE = 1e3;
@@ -22,6 +28,8 @@ var MAX_REQUEST_BYTES = 512 * 1024;
22
28
  var MAX_ERROR_MESSAGE_BYTES = 1e3;
23
29
  var MAX_EMAIL_LENGTH = 254;
24
30
  var MAX_EVENT_NAME_LENGTH = 100;
31
+ var MAX_CUSTOMER_ID_LENGTH = 128;
32
+ var MAX_CUSTOMER_NAME_LENGTH = 200;
25
33
  var MAX_EXIT_DRAINS = 3;
26
34
  var NEXT_BATCH_DELAY_MS = 10;
27
35
  var EXIT_PROBE_MS = 100;
@@ -31,12 +39,15 @@ var KEEPALIVE_BUDGET_BYTES = 32 * 1024;
31
39
  var WAIT_TIMEOUT_MS = 2e3;
32
40
  var MAX_TIMER_MS = 2147483647;
33
41
  var MODULE_LOADED_AT = Date.now();
42
+ var MAX_SURFACE_METHODS = 500;
43
+ var MAX_METHOD_NAME_LENGTH = 255;
44
+ var MAX_SURFACE_CHARS = 32 * 1024;
34
45
  function _clamp(value, min, max, fallback) {
35
46
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
36
47
  return Math.min(max, Math.max(min, Math.floor(value)));
37
48
  }
38
49
  function _mergeDefaults(config) {
39
- var _a, _b;
50
+ var _a, _b, _c;
40
51
  return {
41
52
  apiKey: config.apiKey,
42
53
  sdkName: config.sdkName,
@@ -48,7 +59,9 @@ function _mergeDefaults(config) {
48
59
  // A batch size below 1 would never empty the queue; above 100 the endpoint rejects it.
49
60
  batchSize: _clamp(config.batchSize, 1, MAX_BATCH_SIZE, 10),
50
61
  // A tiny interval would send constantly; one too large for a timer would fire every 1 ms.
51
- flushInterval: _clamp(config.flushInterval, 100, MAX_TIMER_MS, 5e3)
62
+ flushInterval: _clamp(config.flushInterval, 100, MAX_TIMER_MS, 5e3),
63
+ advisories: (_c = config.advisories) != null ? _c : true,
64
+ onAdvisory: typeof config.onAdvisory === "function" ? config.onAdvisory : null
52
65
  };
53
66
  }
54
67
  function _generateSessionId() {
@@ -91,6 +104,29 @@ function _getMetadata() {
91
104
  return meta;
92
105
  }
93
106
  var EMAIL_PATTERN = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
107
+ function _customerField(value, maxLength) {
108
+ if (typeof value !== "string") return "";
109
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").trim().slice(0, maxLength).trim();
110
+ }
111
+ function _cleanCustomer(customer) {
112
+ if (customer === null || typeof customer !== "object") return null;
113
+ const c = customer;
114
+ const id = _customerField(c["id"], MAX_CUSTOMER_ID_LENGTH);
115
+ if (id === "") {
116
+ _log("Ignored a customer without an id");
117
+ return null;
118
+ }
119
+ if (EMAIL_PATTERN.test(id)) {
120
+ _log("Ignored a customer id that contains an email address");
121
+ return null;
122
+ }
123
+ const name = _customerField(c["name"], MAX_CUSTOMER_NAME_LENGTH);
124
+ if (name !== "" && EMAIL_PATTERN.test(name)) {
125
+ _log("Left out a customer name that contains an email address");
126
+ return { id };
127
+ }
128
+ return name !== "" ? { id, name } : { id };
129
+ }
94
130
  function _truncateUtf8(str, maxBytes) {
95
131
  let bytes = 0;
96
132
  for (let i = 0; i < str.length; i++) {
@@ -184,6 +220,7 @@ function _enqueue(event) {
184
220
  _log("Queue full; dropped event", event.event);
185
221
  return;
186
222
  }
223
+ if (_customer) event.customer = _customer;
187
224
  let json;
188
225
  try {
189
226
  json = JSON.stringify(event);
@@ -281,6 +318,37 @@ function _createWrapper(methodName, fn) {
281
318
  });
282
319
  return wrapped;
283
320
  }
321
+ function _isMethodName(name) {
322
+ return typeof name === "string" && name.trim() !== "" && name.length <= MAX_METHOD_NAME_LENGTH;
323
+ }
324
+ function _surfaceList(names) {
325
+ const out = /* @__PURE__ */ new Set();
326
+ let chars = 0;
327
+ for (const name of names) {
328
+ if (out.size >= MAX_SURFACE_METHODS) break;
329
+ if (!_isMethodName(name) || out.has(name)) continue;
330
+ if (chars + name.length > MAX_SURFACE_CHARS) break;
331
+ out.add(name);
332
+ chars += name.length;
333
+ }
334
+ return Array.from(out);
335
+ }
336
+ function _register(methodName) {
337
+ if (!_isMethodName(methodName) || _registered.has(methodName) || _registered.size >= MAX_SURFACE_METHODS) return;
338
+ _registered.add(methodName);
339
+ if (!_config) return;
340
+ _undeclared.push(methodName);
341
+ if (_declareScheduled) return;
342
+ _declareScheduled = true;
343
+ Promise.resolve().then(
344
+ () => _safely(() => {
345
+ _declareScheduled = false;
346
+ const methods = _surfaceList(_undeclared);
347
+ _undeclared = [];
348
+ if (methods.length > 0) SdkGarden.track("api_surface", { api_surface: methods });
349
+ })
350
+ );
351
+ }
284
352
  function _keepaliveBytesInFlight() {
285
353
  let total = 0;
286
354
  _pending.forEach((request) => {
@@ -297,6 +365,55 @@ function _takeBatch(events, maxEvents) {
297
365
  }
298
366
  return events.splice(0, count).map((event) => event.json);
299
367
  }
368
+ function _parseAdvisory(raw) {
369
+ var _a;
370
+ const fields = {};
371
+ for (const part of raw.split("&")) {
372
+ const eq = part.indexOf("=");
373
+ if (eq <= 0) continue;
374
+ try {
375
+ fields[decodeURIComponent(part.slice(0, eq))] = decodeURIComponent(part.slice(eq + 1).replace(/\+/g, " "));
376
+ } catch (e) {
377
+ return null;
378
+ }
379
+ }
380
+ const severity = fields["severity"];
381
+ if (!fields["version"] || severity !== "deprecated" && severity !== "broken" && severity !== "security") {
382
+ return null;
383
+ }
384
+ return {
385
+ version: fields["version"],
386
+ severity,
387
+ message: ((_a = fields["message"]) != null ? _a : "").slice(0, 280),
388
+ ...fields["upgrade_to"] ? { upgradeTo: fields["upgrade_to"].slice(0, 50) } : {}
389
+ };
390
+ }
391
+ function _advisoryText(sdkName, a) {
392
+ const what = { deprecated: "is deprecated", broken: "has a known problem", security: "has a security issue" }[a.severity];
393
+ let text = `[${sdkName}] Version ${a.version} ${what}.`;
394
+ if (a.message) text += ` ${a.message}`;
395
+ if (a.upgradeTo) text += ` Upgrade to ${a.upgradeTo}.`;
396
+ return text;
397
+ }
398
+ function _handleAdvisory(config, response) {
399
+ var _a, _b;
400
+ if (!config.advisories) return;
401
+ const raw = (_b = (_a = response == null ? void 0 : response.headers) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, ADVISORY_HEADER);
402
+ if (typeof raw !== "string" || raw === "") return;
403
+ const advisory = _parseAdvisory(raw);
404
+ if (!advisory || _advisoriesShown.has(advisory.version)) return;
405
+ _advisoriesShown.add(advisory.version);
406
+ if (config.onAdvisory) {
407
+ try {
408
+ config.onAdvisory(advisory);
409
+ } catch (e) {
410
+ }
411
+ return;
412
+ }
413
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
414
+ console.warn(_advisoryText(config.sdkName, advisory));
415
+ }
416
+ }
300
417
  function _send(config, batch, keepalive) {
301
418
  var _a;
302
419
  if (typeof fetch !== "function") {
@@ -361,6 +478,7 @@ function _send(config, batch, keepalive) {
361
478
  if (stream && typeof stream.cancel === "function") discarded = stream.cancel();
362
479
  } catch (e) {
363
480
  }
481
+ if (delivered) _safely(() => _handleAdvisory(config, response));
364
482
  Promise.resolve(discarded).then(settle, settle);
365
483
  },
366
484
  () => settle()
@@ -562,6 +680,7 @@ function _teardown() {
562
680
  _config = null;
563
681
  _queue = [];
564
682
  _sessionId = "";
683
+ _customer = null;
565
684
  _endExit();
566
685
  }
567
686
  function _start(config) {
@@ -583,6 +702,12 @@ function _start(config) {
583
702
  _config = _mergeDefaults(config);
584
703
  _queue = carried;
585
704
  _sessionId = _generateSessionId();
705
+ _customer = config.customer === void 0 ? null : _cleanCustomer(config.customer);
706
+ _undeclared = [];
707
+ const apiSurface = _surfaceList([
708
+ ...Array.isArray(config.methods) ? config.methods : [],
709
+ ..._registered
710
+ ]);
586
711
  _log("Initialised", _sanitizeConfig(_config));
587
712
  _enqueue({
588
713
  event: "sdk_initialized",
@@ -591,7 +716,8 @@ function _start(config) {
591
716
  timestamp: Date.now(),
592
717
  properties: {
593
718
  environment: _getEnvironment(),
594
- time_to_init_ms: Math.max(0, Date.now() - initStartedAt)
719
+ time_to_init_ms: Math.max(0, Date.now() - initStartedAt),
720
+ ...apiSurface.length > 0 ? { api_surface: apiSurface } : {}
595
721
  },
596
722
  meta: _getMetadata()
597
723
  });
@@ -654,6 +780,7 @@ var SdkGarden = {
654
780
  * ```
655
781
  */
656
782
  trackMethod(methodName, fn) {
783
+ _safely(() => _register(methodName));
657
784
  if (fn !== void 0) {
658
785
  return _createWrapper(methodName, fn);
659
786
  }
@@ -688,6 +815,25 @@ var SdkGarden = {
688
815
  })
689
816
  );
690
817
  },
818
+ /**
819
+ * Attribute the session to one of your customers: an app or account, by an
820
+ * ID from your own system. Every event tracked afterwards carries it, so the
821
+ * dashboard can show which customers are on old versions, still call
822
+ * deprecated methods or hit errors. Pass null to stop. Lasts until the next
823
+ * init() or shutdown(); no-op before init().
824
+ *
825
+ * Never pass an email address or other personal data: an ID containing an
826
+ * email address is ignored.
827
+ *
828
+ * @example
829
+ * SdkGarden.setCustomer({ id: account.id, name: account.displayName });
830
+ */
831
+ setCustomer(customer) {
832
+ if (!_config) return;
833
+ _safely(() => {
834
+ _customer = customer === null ? null : _cleanCustomer(customer);
835
+ });
836
+ },
691
837
  /**
692
838
  * Send every queued event now and wait for the requests to finish.
693
839
  * Useful at the end of a serverless handler or a test.
@@ -722,12 +868,15 @@ var SdkGarden = {
722
868
  /**
723
869
  * Reset all internal state.
724
870
  * FOR UNIT TESTS ONLY — not documented in README.
725
- * Clears queue, config, session ID, flush timer, listeners and the record
726
- * of requests in flight, without sending or cancelling anything.
871
+ * Clears queue, config, session ID, flush timer, listeners, registered
872
+ * method names and the record of requests in flight, without sending or cancelling anything.
727
873
  */
728
874
  _reset() {
729
875
  _teardown();
730
876
  _pending.clear();
877
+ _registered.clear();
878
+ _undeclared = [];
879
+ _advisoriesShown.clear();
731
880
  }
732
881
  };
733
882
  var index_default = SdkGarden;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdk-garden-js",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Lightweight SDK observability client for JavaScript/TypeScript \u2014 zero runtime dependencies",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",