sdk-garden-js 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,106 @@
1
- # Temporary Holding Version
1
+ # sdk-garden-js
2
+ *Lightweight SDK observability client for JavaScript and TypeScript*
2
3
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
+ ## Install
5
+
6
+ ```bash
7
+ npm install sdk-garden-js
8
+ ```
9
+
10
+ ## Quick Start
11
+
12
+ **ESM (TypeScript / modern bundlers):**
13
+
14
+ ```typescript
15
+ import SdkGarden from 'sdk-garden-js';
16
+
17
+ SdkGarden.init({
18
+ apiKey: 'YOUR_API_KEY',
19
+ sdkName: 'my-sdk',
20
+ sdkVersion: '1.0.0',
21
+ });
22
+
23
+ // Decorator form — annotate class methods directly:
24
+ class PaymentsSDK {
25
+ @SdkGarden.trackMethod('processPayment')
26
+ processPayment(amount: number, currency: string): Receipt {
27
+ // your implementation
28
+ }
29
+ }
30
+
31
+ // Wrapper form — wrap standalone functions:
32
+ export const trackedProcess = SdkGarden.trackMethod('processPayment', processPayment);
33
+ ```
34
+
35
+ **CommonJS (Node.js / require):**
36
+
37
+ ```javascript
38
+ const SdkGarden = require('sdk-garden-js');
39
+
40
+ SdkGarden.init({
41
+ apiKey: 'YOUR_API_KEY',
42
+ sdkName: 'my-sdk',
43
+ sdkVersion: '1.0.0',
44
+ });
45
+ ```
46
+
47
+ ## Configuration
48
+
49
+ | Parameter | Type | Default | Description |
50
+ |-----------|------|---------|-------------|
51
+ | `apiKey` | `string` | _(required)_ | Your SDK Garden API key (starts with `sgk_`) |
52
+ | `sdkName` | `string` | _(required)_ | The name of your SDK (e.g. `"my-payments-sdk"`) |
53
+ | `sdkVersion` | `string` | _(required)_ | The version of your SDK (e.g. `"1.2.3"`) |
54
+ | `endpoint` | `string` | `"https://ingest.sdkgarden.dev/track"` | Override the ingest endpoint |
55
+ | `debug` | `boolean` | `false` | Enable debug logging to console |
56
+ | `disabled` | `boolean` | `false` | When `true`, all methods become no-ops |
57
+ | `batchSize` | `number` | `10` | Maximum events per flush batch |
58
+ | `flushInterval` | `number` | `5000` | Flush interval in milliseconds |
59
+ | `startedAt` | `number` | module load time | `Date.now()` when your SDK began initializing; reported as `time_to_init_ms` for the dashboard's init timing |
60
+
61
+ ## What gets sent
62
+
63
+ 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:
64
+
65
+ - **`sdk_initialized`**: runtime (`node` + Node version, or `browser` + screen/viewport size, pixel ratio, timezone and language) and `time_to_init_ms`.
66
+ - **`method_called` / `method_success`**: method name, duration, sync/async, and the *number* of arguments.
67
+ - **`sdk_error`**: error type and message (email addresses are redacted) plus the method and duration.
68
+ - **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.
69
+
70
+ Never sent: argument or return values, request or response bodies, cookies, stack traces, or user identifiers. Nothing is sent when `disabled: true`.
71
+
72
+ ### Telling your users
73
+
74
+ Copy this into your SDK's README or privacy docs so your users know what's collected (edit the names to match):
75
+
76
+ > **Telemetry.** This SDK reports anonymous usage data to its maintainers through [SDK Garden](https://github.com/tjb/SDKGarden): the SDK version, runtime version, which SDK methods are called, how long they take, and error types. It never sends your data, request contents or personal information. To opt out, set `YOUR_SDK_TELEMETRY_DISABLED=1` *(wire this environment variable to `disabled: true` in your SDK)*.
77
+
78
+ ## Silent Failure Guarantee
79
+
80
+ All network errors are swallowed silently. SDK Garden will never throw or reject in your caller's context.
81
+
82
+ Pass `disabled: true` in test environments to suppress all network calls.
83
+
84
+ ## Known Limitations
85
+
86
+ ### Async fire-and-forget calls (wrapper form only)
87
+
88
+ When using the **wrapper form**, `trackMethod` records `method_success` only when the caller `await`s or chains `.then()` on the returned promise. If the promise is discarded, `method_called` fires but `method_success` is never enqueued.
89
+
90
+ **Affected pattern:**
91
+ ```typescript
92
+ // method_called fires, but method_success is NEVER recorded:
93
+ SdkGarden.trackMethod('notify', notify)(...args); // promise discarded
94
+ ```
95
+
96
+ **Not affected:**
97
+ ```typescript
98
+ // Both events recorded correctly:
99
+ await SdkGarden.trackMethod('notify', notify)(...args);
100
+ ```
101
+
102
+ The **decorator form** does not have this limitation — class method decorators always capture the returned promise correctly regardless of how the caller uses it. Synchronous functions are not affected in either form.
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,92 @@
1
+ /**
2
+ * sdk-garden-js — Lightweight SDK observability client for JavaScript and TypeScript.
3
+ *
4
+ * Drop-in singleton for SDK publishers to instrument their own SDKs with
5
+ * initialization, method call, and error telemetry.
6
+ *
7
+ * Security:
8
+ * - API key is never logged raw (see _sanitizeConfig).
9
+ * - Error messages are PII-scrubbed (email addresses redacted) before enqueueing.
10
+ * - All network errors in _flush() are swallowed silently — never propagated to callers.
11
+ */
12
+ /** Configuration for SdkGarden.init(). */
13
+ interface SdkGardenConfig {
14
+ /** Your SDK Garden API key (starts with sgk_). */
15
+ apiKey: string;
16
+ /** The name of your SDK (e.g. "my-payments-sdk"). */
17
+ sdkName: string;
18
+ /** The version of your SDK (e.g. "1.2.3"). */
19
+ sdkVersion: string;
20
+ /** Override the default ingest endpoint. Default: https://ingest.sdkgarden.dev/track */
21
+ endpoint?: string;
22
+ /** Enable debug logging to console. Default: false */
23
+ debug?: boolean;
24
+ /** When true, all methods become no-ops — no events, no network calls. Default: false */
25
+ disabled?: boolean;
26
+ /** Maximum events per flush batch. Default: 10 */
27
+ batchSize?: number;
28
+ /** Flush interval in milliseconds. Default: 5000 */
29
+ flushInterval?: number;
30
+ /**
31
+ * When your SDK started initializing, as a `Date.now()` timestamp. Used for
32
+ * the dashboard's init-timing panel (`time_to_init_ms` = init() time minus
33
+ * this). Capture it at the top of your SDK's constructor or setup function.
34
+ * Default: when sdk-garden-js was first loaded.
35
+ */
36
+ startedAt?: number;
37
+ }
38
+ /**
39
+ * Stage 3 class method decorator returned by SdkGarden.trackMethod(name).
40
+ *
41
+ * @example
42
+ * class MySDK {
43
+ * \@SdkGarden.trackMethod('processPayment')
44
+ * processPayment(amount: number): Receipt { ... }
45
+ * }
46
+ */
47
+ type SdkMethodDecorator = <This, Args extends unknown[], Return>(fn: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>) => (this: This, ...args: Args) => Return;
48
+ /**
49
+ * Fire-and-forget flush to the ingest endpoint.
50
+ * Ported from sdk/sdk-garden.js lines 232–270, simplified per plan:
51
+ * - No retry on 429 (v1)
52
+ * - No re-queue on failure (events are discarded)
53
+ * - Silent failure: catch() swallows all errors
54
+ *
55
+ * NOTE: _flush() is also exposed on the SdkGarden object for test access.
56
+ */
57
+ declare function _flush(): void;
58
+ /**
59
+ * Typed interface for the SdkGarden singleton.
60
+ * Declared separately so that trackMethod can have proper overload signatures —
61
+ * TypeScript does not support method overloads inside object literals.
62
+ */
63
+ interface SdkGardenObject {
64
+ init(config: SdkGardenConfig): void;
65
+ track(eventName: string, properties?: Record<string, unknown>): void;
66
+ /** Decorator form — use as @SdkGarden.trackMethod('name') on a class method. */
67
+ trackMethod(methodName: string): SdkMethodDecorator;
68
+ /** Wrapper form — pass an existing function, receive the instrumented version. */
69
+ trackMethod<T extends (...args: unknown[]) => unknown>(methodName: string, fn: T): T;
70
+ trackError(error: Error, context?: Record<string, unknown>): void;
71
+ _flush: typeof _flush;
72
+ _reset(): void;
73
+ }
74
+ /**
75
+ * SdkGarden — singleton SDK observability client.
76
+ *
77
+ * @example
78
+ * import SdkGarden from 'sdk-garden-js';
79
+ * SdkGarden.init({ apiKey: 'sgk_...', sdkName: 'my-sdk', sdkVersion: '1.0.0' });
80
+ *
81
+ * // Decorator form (class methods):
82
+ * class MySDK {
83
+ * \@SdkGarden.trackMethod('processPayment')
84
+ * processPayment(amount: number): Receipt { ... }
85
+ * }
86
+ *
87
+ * // Wrapper form (standalone functions):
88
+ * const tracked = SdkGarden.trackMethod('processPayment', processPayment);
89
+ */
90
+ declare const SdkGarden: SdkGardenObject;
91
+
92
+ export { type SdkGardenConfig, type SdkMethodDecorator, SdkGarden as default };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * sdk-garden-js — Lightweight SDK observability client for JavaScript and TypeScript.
3
+ *
4
+ * Drop-in singleton for SDK publishers to instrument their own SDKs with
5
+ * initialization, method call, and error telemetry.
6
+ *
7
+ * Security:
8
+ * - API key is never logged raw (see _sanitizeConfig).
9
+ * - Error messages are PII-scrubbed (email addresses redacted) before enqueueing.
10
+ * - All network errors in _flush() are swallowed silently — never propagated to callers.
11
+ */
12
+ /** Configuration for SdkGarden.init(). */
13
+ interface SdkGardenConfig {
14
+ /** Your SDK Garden API key (starts with sgk_). */
15
+ apiKey: string;
16
+ /** The name of your SDK (e.g. "my-payments-sdk"). */
17
+ sdkName: string;
18
+ /** The version of your SDK (e.g. "1.2.3"). */
19
+ sdkVersion: string;
20
+ /** Override the default ingest endpoint. Default: https://ingest.sdkgarden.dev/track */
21
+ endpoint?: string;
22
+ /** Enable debug logging to console. Default: false */
23
+ debug?: boolean;
24
+ /** When true, all methods become no-ops — no events, no network calls. Default: false */
25
+ disabled?: boolean;
26
+ /** Maximum events per flush batch. Default: 10 */
27
+ batchSize?: number;
28
+ /** Flush interval in milliseconds. Default: 5000 */
29
+ flushInterval?: number;
30
+ /**
31
+ * When your SDK started initializing, as a `Date.now()` timestamp. Used for
32
+ * the dashboard's init-timing panel (`time_to_init_ms` = init() time minus
33
+ * this). Capture it at the top of your SDK's constructor or setup function.
34
+ * Default: when sdk-garden-js was first loaded.
35
+ */
36
+ startedAt?: number;
37
+ }
38
+ /**
39
+ * Stage 3 class method decorator returned by SdkGarden.trackMethod(name).
40
+ *
41
+ * @example
42
+ * class MySDK {
43
+ * \@SdkGarden.trackMethod('processPayment')
44
+ * processPayment(amount: number): Receipt { ... }
45
+ * }
46
+ */
47
+ type SdkMethodDecorator = <This, Args extends unknown[], Return>(fn: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>) => (this: This, ...args: Args) => Return;
48
+ /**
49
+ * Fire-and-forget flush to the ingest endpoint.
50
+ * Ported from sdk/sdk-garden.js lines 232–270, simplified per plan:
51
+ * - No retry on 429 (v1)
52
+ * - No re-queue on failure (events are discarded)
53
+ * - Silent failure: catch() swallows all errors
54
+ *
55
+ * NOTE: _flush() is also exposed on the SdkGarden object for test access.
56
+ */
57
+ declare function _flush(): void;
58
+ /**
59
+ * Typed interface for the SdkGarden singleton.
60
+ * Declared separately so that trackMethod can have proper overload signatures —
61
+ * TypeScript does not support method overloads inside object literals.
62
+ */
63
+ interface SdkGardenObject {
64
+ init(config: SdkGardenConfig): void;
65
+ track(eventName: string, properties?: Record<string, unknown>): void;
66
+ /** Decorator form — use as @SdkGarden.trackMethod('name') on a class method. */
67
+ trackMethod(methodName: string): SdkMethodDecorator;
68
+ /** Wrapper form — pass an existing function, receive the instrumented version. */
69
+ trackMethod<T extends (...args: unknown[]) => unknown>(methodName: string, fn: T): T;
70
+ trackError(error: Error, context?: Record<string, unknown>): void;
71
+ _flush: typeof _flush;
72
+ _reset(): void;
73
+ }
74
+ /**
75
+ * SdkGarden — singleton SDK observability client.
76
+ *
77
+ * @example
78
+ * import SdkGarden from 'sdk-garden-js';
79
+ * SdkGarden.init({ apiKey: 'sgk_...', sdkName: 'my-sdk', sdkVersion: '1.0.0' });
80
+ *
81
+ * // Decorator form (class methods):
82
+ * class MySDK {
83
+ * \@SdkGarden.trackMethod('processPayment')
84
+ * processPayment(amount: number): Receipt { ... }
85
+ * }
86
+ *
87
+ * // Wrapper form (standalone functions):
88
+ * const tracked = SdkGarden.trackMethod('processPayment', processPayment);
89
+ */
90
+ declare const SdkGarden: SdkGardenObject;
91
+
92
+ export { type SdkGardenConfig, type SdkMethodDecorator, SdkGarden as default };
package/dist/index.js ADDED
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ default: () => index_default
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var _config = null;
27
+ var _queue = [];
28
+ var _sessionId = "";
29
+ var _flushTimer = null;
30
+ var _unloadHandler = null;
31
+ var DEFAULT_ENDPOINT = "https://ingest.sdkgarden.dev/track";
32
+ var MODULE_LOADED_AT = Date.now();
33
+ function _mergeDefaults(config) {
34
+ var _a, _b, _c, _d, _e;
35
+ return {
36
+ apiKey: config.apiKey,
37
+ sdkName: config.sdkName,
38
+ 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
44
+ };
45
+ }
46
+ function _generateSessionId() {
47
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
48
+ return crypto.randomUUID();
49
+ }
50
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
51
+ }
52
+ function _getEnvironment() {
53
+ var _a, _b, _c, _d;
54
+ if (typeof window === "undefined") {
55
+ const nodeVersion = typeof process !== "undefined" ? (_a = process.versions) == null ? void 0 : _a.node : void 0;
56
+ return nodeVersion ? { runtime: "node", runtime_version: nodeVersion } : { runtime: "node" };
57
+ }
58
+ const w = window;
59
+ return {
60
+ runtime: "browser",
61
+ screen_width: (_b = w.screen) == null ? void 0 : _b.width,
62
+ screen_height: (_c = w.screen) == null ? void 0 : _c.height,
63
+ viewport_width: w.innerWidth,
64
+ viewport_height: w.innerHeight,
65
+ color_depth: (_d = w.screen) == null ? void 0 : _d.colorDepth,
66
+ pixel_ratio: w.devicePixelRatio,
67
+ timezone: typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : void 0,
68
+ language: typeof navigator !== "undefined" ? navigator.language : void 0
69
+ };
70
+ }
71
+ function _getMetadata() {
72
+ var _a;
73
+ const meta = {};
74
+ if (typeof window !== "undefined") {
75
+ const origin = (_a = window.location) == null ? void 0 : _a.origin;
76
+ if (origin && origin !== "null") {
77
+ meta["url"] = origin;
78
+ }
79
+ }
80
+ return meta;
81
+ }
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]"
87
+ );
88
+ }
89
+ function _sanitizeConfig(config) {
90
+ const sanitized = { ...config };
91
+ const sensitiveKeys = ["apiKey", "api_key", "secret", "password", "token"];
92
+ for (const key of Object.keys(sanitized)) {
93
+ if (sensitiveKeys.some((sk) => key.toLowerCase().includes(sk))) {
94
+ sanitized[key] = "***REDACTED***";
95
+ }
96
+ }
97
+ return sanitized;
98
+ }
99
+ function _log(...args) {
100
+ if (_config == null ? void 0 : _config.debug) {
101
+ console.log("[SDK Garden]", ...args);
102
+ }
103
+ }
104
+ function _enqueue(event) {
105
+ _queue.push(event);
106
+ _log("Event tracked", event.event);
107
+ if (_config && _queue.length >= _config.batchSize) {
108
+ _flush();
109
+ }
110
+ }
111
+ 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;
117
+ 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;
124
+ }
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
+ });
141
+ }
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
148
+ });
149
+ return result;
150
+ };
151
+ }
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(() => {
163
+ });
164
+ }
165
+ var SdkGarden = {
166
+ /**
167
+ * 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.
170
+ */
171
+ 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);
203
+ }
204
+ },
205
+ /**
206
+ * Track a custom event.
207
+ * No-op if SDK has not been initialised or is disabled.
208
+ */
209
+ 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
+ });
219
+ },
220
+ /**
221
+ * Instrument a method with call, success, and error telemetry.
222
+ *
223
+ * **Two-argument form** — wraps a standalone function:
224
+ * ```ts
225
+ * function processPayment(amount: number): Receipt { ... }
226
+ * export const trackedProcessPayment = SdkGarden.trackMethod('processPayment', processPayment);
227
+ * ```
228
+ *
229
+ * **One-argument form** — Stage 3 class method decorator:
230
+ * ```ts
231
+ * class MySDK {
232
+ * \@SdkGarden.trackMethod('processPayment')
233
+ * processPayment(amount: number): Receipt { ... }
234
+ * }
235
+ * ```
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
+ */
241
+ trackMethod(methodName, fn) {
242
+ if (fn !== void 0) {
243
+ if (!_config || _config.disabled) return fn;
244
+ return _createWrapper(methodName, fn);
245
+ }
246
+ return function(target, _context) {
247
+ return _createWrapper(methodName, target);
248
+ };
249
+ },
250
+ /**
251
+ * Track an error event.
252
+ * Applies PII scrubbing to error.message before enqueueing.
253
+ * No-op if SDK is uninitialised or disabled.
254
+ */
255
+ 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()
268
+ });
269
+ },
270
+ /**
271
+ * Manually trigger a flush of queued events.
272
+ * Useful in beforeunload handlers or at process exit.
273
+ */
274
+ _flush,
275
+ /**
276
+ * Reset all internal state.
277
+ * FOR UNIT TESTS ONLY — not documented in README.
278
+ * Clears queue, config, session ID, and flush timer.
279
+ */
280
+ _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 = "";
292
+ }
293
+ };
294
+ var index_default = SdkGarden;
package/dist/index.mjs ADDED
@@ -0,0 +1,273 @@
1
+ // src/index.ts
2
+ var _config = null;
3
+ var _queue = [];
4
+ var _sessionId = "";
5
+ var _flushTimer = null;
6
+ var _unloadHandler = null;
7
+ var DEFAULT_ENDPOINT = "https://ingest.sdkgarden.dev/track";
8
+ var MODULE_LOADED_AT = Date.now();
9
+ function _mergeDefaults(config) {
10
+ var _a, _b, _c, _d, _e;
11
+ return {
12
+ apiKey: config.apiKey,
13
+ sdkName: config.sdkName,
14
+ sdkVersion: config.sdkVersion,
15
+ endpoint: (_a = config.endpoint) != null ? _a : DEFAULT_ENDPOINT,
16
+ debug: (_b = config.debug) != null ? _b : false,
17
+ disabled: (_c = config.disabled) != null ? _c : false,
18
+ batchSize: (_d = config.batchSize) != null ? _d : 10,
19
+ flushInterval: (_e = config.flushInterval) != null ? _e : 5e3
20
+ };
21
+ }
22
+ function _generateSessionId() {
23
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
24
+ return crypto.randomUUID();
25
+ }
26
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
27
+ }
28
+ function _getEnvironment() {
29
+ var _a, _b, _c, _d;
30
+ if (typeof window === "undefined") {
31
+ const nodeVersion = typeof process !== "undefined" ? (_a = process.versions) == null ? void 0 : _a.node : void 0;
32
+ return nodeVersion ? { runtime: "node", runtime_version: nodeVersion } : { runtime: "node" };
33
+ }
34
+ const w = window;
35
+ return {
36
+ runtime: "browser",
37
+ screen_width: (_b = w.screen) == null ? void 0 : _b.width,
38
+ screen_height: (_c = w.screen) == null ? void 0 : _c.height,
39
+ viewport_width: w.innerWidth,
40
+ viewport_height: w.innerHeight,
41
+ color_depth: (_d = w.screen) == null ? void 0 : _d.colorDepth,
42
+ pixel_ratio: w.devicePixelRatio,
43
+ timezone: typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : void 0,
44
+ language: typeof navigator !== "undefined" ? navigator.language : void 0
45
+ };
46
+ }
47
+ function _getMetadata() {
48
+ var _a;
49
+ const meta = {};
50
+ if (typeof window !== "undefined") {
51
+ const origin = (_a = window.location) == null ? void 0 : _a.origin;
52
+ if (origin && origin !== "null") {
53
+ meta["url"] = origin;
54
+ }
55
+ }
56
+ return meta;
57
+ }
58
+ function _scrubPII(str) {
59
+ if (typeof str !== "string") return str;
60
+ return str.replace(
61
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
62
+ "[REDACTED]"
63
+ );
64
+ }
65
+ function _sanitizeConfig(config) {
66
+ const sanitized = { ...config };
67
+ const sensitiveKeys = ["apiKey", "api_key", "secret", "password", "token"];
68
+ for (const key of Object.keys(sanitized)) {
69
+ if (sensitiveKeys.some((sk) => key.toLowerCase().includes(sk))) {
70
+ sanitized[key] = "***REDACTED***";
71
+ }
72
+ }
73
+ return sanitized;
74
+ }
75
+ function _log(...args) {
76
+ if (_config == null ? void 0 : _config.debug) {
77
+ console.log("[SDK Garden]", ...args);
78
+ }
79
+ }
80
+ function _enqueue(event) {
81
+ _queue.push(event);
82
+ _log("Event tracked", event.event);
83
+ if (_config && _queue.length >= _config.batchSize) {
84
+ _flush();
85
+ }
86
+ }
87
+ function _createWrapper(methodName, fn) {
88
+ return function(...args) {
89
+ if (!_config || _config.disabled) return fn.call(this, ...args);
90
+ const startTime = typeof performance !== "undefined" ? performance.now() : Date.now();
91
+ SdkGarden.track("method_called", { method: methodName, args_count: args.length });
92
+ let result;
93
+ try {
94
+ result = fn.call(this, ...args);
95
+ } catch (error) {
96
+ const duration2 = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
97
+ const err = error instanceof Error ? error : new Error(String(error));
98
+ SdkGarden.trackError(err, { method: methodName, duration_ms: Math.round(duration2), is_async: false });
99
+ throw error;
100
+ }
101
+ if (result !== null && typeof result === "object" && typeof result.then === "function") {
102
+ return result.then((value) => {
103
+ const duration2 = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
104
+ SdkGarden.track("method_success", {
105
+ method: methodName,
106
+ duration_ms: Math.round(duration2),
107
+ is_async: true,
108
+ args_count: args.length
109
+ });
110
+ return value;
111
+ }).catch((error) => {
112
+ const duration2 = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
113
+ const err = error instanceof Error ? error : new Error(String(error));
114
+ SdkGarden.trackError(err, { method: methodName, duration_ms: Math.round(duration2), is_async: true });
115
+ throw error;
116
+ });
117
+ }
118
+ const duration = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
119
+ SdkGarden.track("method_success", {
120
+ method: methodName,
121
+ duration_ms: Math.round(duration),
122
+ is_async: false,
123
+ args_count: args.length
124
+ });
125
+ return result;
126
+ };
127
+ }
128
+ function _flush() {
129
+ if (!_config || _queue.length === 0) return;
130
+ const events = _queue.splice(0, _config.batchSize);
131
+ const payload = JSON.stringify({ api_key: _config.apiKey, events });
132
+ _log("Flushing events", events.length);
133
+ fetch(_config.endpoint, {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/json" },
136
+ body: payload,
137
+ keepalive: true
138
+ }).catch(() => {
139
+ });
140
+ }
141
+ var SdkGarden = {
142
+ /**
143
+ * Initialize the SDK Garden client.
144
+ * Must be called once before any tracking methods.
145
+ * When config.disabled is true, this is a no-op.
146
+ */
147
+ init(config) {
148
+ var _a;
149
+ if (config.disabled) {
150
+ return;
151
+ }
152
+ if (_flushTimer !== null) {
153
+ clearInterval(_flushTimer);
154
+ _flushTimer = null;
155
+ }
156
+ const initStartedAt = (_a = config.startedAt) != null ? _a : MODULE_LOADED_AT;
157
+ _config = _mergeDefaults(config);
158
+ _queue = [];
159
+ _sessionId = _generateSessionId();
160
+ _log("Initialised", _sanitizeConfig(_config));
161
+ _enqueue({
162
+ event: "sdk_initialized",
163
+ sdk: { name: _config.sdkName, version: _config.sdkVersion },
164
+ session_id: _sessionId,
165
+ timestamp: Date.now(),
166
+ properties: {
167
+ environment: _getEnvironment(),
168
+ time_to_init_ms: Math.max(0, Date.now() - initStartedAt)
169
+ },
170
+ meta: _getMetadata()
171
+ });
172
+ _flushTimer = setInterval(_flush, _config.flushInterval);
173
+ if (typeof window !== "undefined") {
174
+ if (_unloadHandler) {
175
+ window.removeEventListener("beforeunload", _unloadHandler);
176
+ }
177
+ _unloadHandler = () => _flush();
178
+ window.addEventListener("beforeunload", _unloadHandler);
179
+ }
180
+ },
181
+ /**
182
+ * Track a custom event.
183
+ * No-op if SDK has not been initialised or is disabled.
184
+ */
185
+ track(eventName, properties) {
186
+ if (!_config || _config.disabled) return;
187
+ _enqueue({
188
+ event: eventName,
189
+ sdk: { name: _config.sdkName, version: _config.sdkVersion },
190
+ session_id: _sessionId,
191
+ timestamp: Date.now(),
192
+ properties: properties != null ? properties : {},
193
+ meta: _getMetadata()
194
+ });
195
+ },
196
+ /**
197
+ * Instrument a method with call, success, and error telemetry.
198
+ *
199
+ * **Two-argument form** — wraps a standalone function:
200
+ * ```ts
201
+ * function processPayment(amount: number): Receipt { ... }
202
+ * export const trackedProcessPayment = SdkGarden.trackMethod('processPayment', processPayment);
203
+ * ```
204
+ *
205
+ * **One-argument form** — Stage 3 class method decorator:
206
+ * ```ts
207
+ * class MySDK {
208
+ * \@SdkGarden.trackMethod('processPayment')
209
+ * processPayment(amount: number): Receipt { ... }
210
+ * }
211
+ * ```
212
+ *
213
+ * **Async limitation (v1):** For async functions in the two-argument form,
214
+ * `method_success` is recorded only when the caller awaits or chains `.then()`.
215
+ * Class method decorators do not have this limitation.
216
+ */
217
+ trackMethod(methodName, fn) {
218
+ if (fn !== void 0) {
219
+ if (!_config || _config.disabled) return fn;
220
+ return _createWrapper(methodName, fn);
221
+ }
222
+ return function(target, _context) {
223
+ return _createWrapper(methodName, target);
224
+ };
225
+ },
226
+ /**
227
+ * Track an error event.
228
+ * Applies PII scrubbing to error.message before enqueueing.
229
+ * No-op if SDK is uninitialised or disabled.
230
+ */
231
+ trackError(error, context) {
232
+ if (!_config || _config.disabled) return;
233
+ _enqueue({
234
+ event: "sdk_error",
235
+ sdk: { name: _config.sdkName, version: _config.sdkVersion },
236
+ session_id: _sessionId,
237
+ timestamp: Date.now(),
238
+ properties: {
239
+ error_message: _scrubPII(error.message),
240
+ error_type: error.name,
241
+ ...context != null ? context : {}
242
+ },
243
+ meta: _getMetadata()
244
+ });
245
+ },
246
+ /**
247
+ * Manually trigger a flush of queued events.
248
+ * Useful in beforeunload handlers or at process exit.
249
+ */
250
+ _flush,
251
+ /**
252
+ * Reset all internal state.
253
+ * FOR UNIT TESTS ONLY — not documented in README.
254
+ * Clears queue, config, session ID, and flush timer.
255
+ */
256
+ _reset() {
257
+ if (_flushTimer !== null) {
258
+ clearInterval(_flushTimer);
259
+ _flushTimer = null;
260
+ }
261
+ if (typeof window !== "undefined" && _unloadHandler) {
262
+ window.removeEventListener("beforeunload", _unloadHandler);
263
+ _unloadHandler = null;
264
+ }
265
+ _config = null;
266
+ _queue = [];
267
+ _sessionId = "";
268
+ }
269
+ };
270
+ var index_default = SdkGarden;
271
+ export {
272
+ index_default as default
273
+ };
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "sdk-garden-js",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Lightweight SDK observability client for JavaScript/TypeScript \u2014 zero runtime dependencies",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "sideEffects": true,
25
+ "license": "MIT",
26
+ "homepage": "https://sdkgarden.dev",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/tjb/SDKGarden.git",
30
+ "directory": "sdk/sdk-garden-js"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/tjb/SDKGarden/issues"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "test": "jest",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "devDependencies": {
41
+ "@types/jest": "^29.0.0",
42
+ "jest": "^29.7.0",
43
+ "jest-environment-node": "^29.0.0",
44
+ "ts-jest": "^29.0.0",
45
+ "tsup": "^8.5.1",
46
+ "typescript": "^5.9.2"
47
+ },
48
+ "jest": {
49
+ "preset": "ts-jest",
50
+ "testEnvironment": "node",
51
+ "testMatch": [
52
+ "**/__tests__/**/*.test.ts"
53
+ ]
54
+ }
55
+ }