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 +24 -1
- package/dist/index.d.mts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +154 -5
- package/dist/index.mjs +154 -5
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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