JavaScript SDK Reference
sdk-garden-js is a lightweight, zero-dependency observability client for JavaScript and TypeScript. Embed it inside your published SDK to send initialization, method-call, and error telemetry to SDK Garden without any impact on your users.
Package: sdk-garden-js
Environments: Node.js 18+ (the client uses the global fetch), all modern browsers, bundlers (Webpack, Vite, esbuild)
Installation
npm install sdk-garden-js
Import styles
ESM / TypeScript (recommended):
import SdkGarden from 'sdk-garden-js';
CommonJS / Node.js:
const SdkGarden = require('sdk-garden-js');
Configuration
All options are passed to SdkGarden.init() as a single config object.
| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
apiKey | string | — | Yes | Your SDK Garden API key. Must start with sgk_. |
sdkName | string | — | Yes | The name of your SDK, e.g. "my-payments-sdk". |
sdkVersion | string | — | Yes | The version of your SDK, e.g. "1.2.3". |
endpoint | string | "https://ingest.sdkgarden.com/track" | No | Override the ingest endpoint. |
debug | boolean | false | No | Print [SDK Garden] prefixed debug lines to console.log. The API key is never logged. |
disabled | boolean | false | No | When true, all methods become no-ops. No timer started, no network calls made. Ideal for test environments. |
batchSize | number | 10 | No | Maximum number of events sent in a single HTTP POST, from 1 to 100. A batch is sent as soon as the queue reaches this size. |
flushInterval | number | 5000 | No | Milliseconds between automatic sends of whatever is queued, at least 100. |
API
SdkGarden.init(config)
Initialize the SDK Garden client. Call it once, when your SDK loads; nothing is recorded before it runs. Calling it again starts a new session: events the previous session queued are still sent, right away if the API key or endpoint changed.
When config.disabled is true, no timer is started and no events are enqueued. If a client was already running, it is stopped and its queued events are dropped without being sent.
init() never throws. In a runtime that refuses timers outside a request handler (such as an edge worker's global scope), the client runs without its timer and sends when a batch fills up or flush() is called.
Parameters:
| Parameter | Type | Description |
|---|---|---|
config | SdkGardenConfig | See Configuration above. |
Returns: void
Example:
import SdkGarden from 'sdk-garden-js';
SdkGarden.init({
apiKey: 'sgk_live_abc123',
sdkName: 'acme-payments',
sdkVersion: '2.1.0',
});
Automatically enqueues sdk_initialized with runtime-specific environment metadata on every successful call. In browser environments this includes screen dimensions, viewport, pixel ratio, timezone, and locale. In Node.js it includes runtime: "node" and runtime_version (the Node.js version) — no browser fields are sent.
SdkGarden.track(eventName, properties?)
Enqueue a custom event. Use this for any event that isn't a method call or error.
Parameters:
| Parameter | Type | Description |
|---|---|---|
eventName | string | Name of the event, e.g. "feature_used" or "config_loaded". |
properties | Record<string, unknown> | Optional key/value metadata attached to the event. |
Returns: void
Behavior:
- No-op if
init()has not been called ordisabledistrue. - Never throws.
- Events are queued in memory and flushed in batches.
Example:
SdkGarden.track('feature_used', {
feature: 'streaming',
model: 'gpt-4',
});
SdkGarden.track('config_loaded', {
has_custom_endpoint: true,
});
SdkGarden.trackMethod(methodName) · SdkGarden.trackMethod(methodName, fn)
Instrument a method to automatically track calls, successes, and errors. Supports two call signatures:
- Decorator —
trackMethod(methodName)returns anSdkMethodDecoratorfor use as a class method decorator, standard (Stage 3) or legacy (experimentalDecorators). - Wrapper —
trackMethod(methodName, fn)wraps a standalone function and returns it with the same type.
Parameters:
| Parameter | Type | Description |
|---|---|---|
methodName | string | The name that will appear in the dashboard for this method. |
fn | T extends (...args: any[]) => any | (Wrapper form only) The function to wrap. Omit when using the decorator form. |
Returns:
- Decorator form:
SdkMethodDecorator— a class method decorator, standard (Stage 3) or legacy (experimentalDecorators). - Wrapper form:
T— the same function type, wrapped with telemetry.
Emits:
| Event | When |
|---|---|
method_called | Every invocation, before the method runs. Includes args_count. |
method_success | When the method returns (or its promise resolves). Includes duration_ms, is_async, args_count. |
sdk_error | When the method throws (or its promise rejects). Includes duration_ms, is_async. The original error is re-thrown. |
Decorator form (recommended for class-based SDKs):
class PaymentsSDK {
@SdkGarden.trackMethod('processPayment')
processPayment(amount: number, currency: string): Receipt {
// your implementation — no changes needed here
}
@SdkGarden.trackMethod('fetchUser')
async fetchUser(userId: string): Promise<User> {
const res = await fetch(`/users/${userId}`);
return res.json();
}
}
The decorator instruments the method in place. Callers use processPayment as normal — the telemetry is invisible. Async methods always capture the returned promise correctly, regardless of how the caller uses it.
Wrapper form (for standalone functions):
function processPayment(amount: number, currency: string): Receipt {
// your implementation
}
export const trackedProcessPayment = SdkGarden.trackMethod('processPayment', processPayment);
The wrapper keeps the function's type, this binding, name, length and any properties attached to it, and new still works. Async functions are tracked to completion whether or not the caller awaits the returned promise.
Promises and thenables: both forms always return exactly what your function returned. A native promise, including one from another realm such as a vm context or an iframe, is watched until it settles, so its outcome and duration are recorded, and properties or handlers your function attached to it keep working. One side effect: because SDK Garden handles the promise's rejection to record it, a rejection that nothing else handles is no longer reported as unhandled. Promise subclasses (such as an API client's response promise, whose own then() may read the response) and other thenables (such as lazy query builders, which only run when .then() is called) are never touched; those calls are recorded as successful when the function returns.
Before init(): Both forms can run before init() is called — at class-definition time or at module level, as above. Calls pass straight through until init() runs and are tracked from then on. They pass straight through again after shutdown().
SdkGarden.trackError(error, context?)
Enqueue an sdk_error event for an error you have already caught. The error is not re-thrown — you control propagation.
Parameters:
| Parameter | Type | Description |
|---|---|---|
error | Error | The error to record. error.message is PII-scrubbed before sending. |
context | Record<string, unknown> | Optional additional metadata merged into the event properties. |
Returns: void
PII scrubbing: Email addresses matching the standard user@example.com pattern are replaced with [REDACTED] in error.message before the event is enqueued. Only the start of the message is sent: its first 1,000 characters, cut to 1,000 UTF-8 bytes, as much as the server keeps. Apart from email addresses the message is sent as it is, so whatever your SDK puts in an error message (a hostname, a URL, an ID) is sent too.
Example:
try {
await connectToDatabase(config);
} catch (err) {
SdkGarden.trackError(err as Error, {
phase: 'startup',
config_has_tls: config.tls,
});
throw err; // you decide whether to re-raise
}
SdkGarden.flush(timeoutMs?)
Send every queued event now and wait for the requests to finish. Useful at the end of a serverless handler, a short script or a test.
Parameters:
| Parameter | Type | Description |
|---|---|---|
timeoutMs | number | Optional. How long to wait, in milliseconds. Default 2000. Requests still running after that keep going in the background until they finish or time out. |
Returns: Promise<void> that resolves once the requests have finished or timeoutMs has passed. It never rejects.
export async function handler(event) {
const result = await doWork(event);
await SdkGarden.flush();
return result;
}
SdkGarden.shutdown(timeoutMs?)
Send every queued event, then stop the client: the flush timer and the exit and page listeners are removed, and track, trackError and tracked methods do nothing until init() is called again.
Parameters:
| Parameter | Type | Description |
|---|---|---|
timeoutMs | number | Optional. Default 2000. Requests still running after this are abandoned, so a Node.js process can exit. Pass 0 to stop at once: queued events are dropped and requests in flight are aborted. |
Returns: Promise<void> that never rejects. Calling it again is harmless.
class PaymentsSDK {
async close(): Promise<void> {
await this.connection.close();
await SdkGarden.shutdown();
}
}
Process exit and page unload
You usually do not need either method:
- Node.js: the client never keeps a process alive on its own. When a program runs out of work, Node emits
beforeExit, and the client sends whatever is still queued before the process exits, including events your ownbeforeExithandlers track, in up to 10 requests at once. In Node.js a request times out after 2 seconds, so an endpoint that does not answer delays exit by about 2 seconds, or about 4 if a request was already running. A DNS server that never answers can hold the process for the system's DNS timeout, since a lookup cannot be cancelled. - Browsers: the client sends queued events when the page is hidden (
visibilitychange) or unloaded (pagehide), usingfetchwithkeepaliveso the request outlives the page. Only these sends usekeepalive. Browsers limit how muchkeepalivedata a page may have in flight, so the client uses at most half of it and leaves the rest to the page.
process.exit() and signals such as Ctrl-C end a Node.js process without beforeExit. If your SDK's users may exit that way, give your SDK a close() that awaits SdkGarden.shutdown().
beforeExit handlers and job runners. Each send makes Node emit beforeExit again once it finishes. So that a beforeExit handler that tracks events or calls flush() every time cannot keep the process alive, at most three sends go out from beforeExit while a process exits (the client's own, and flush() calls made in beforeExit handlers), none after a failed one or after 2 seconds, and events tracked meanwhile wait for one of those sends. Once the process shows that it carried on, by running for another second or by being frozen (as AWS Lambda freezes a callback handler's process between invocations), normal sending resumes. A flush() called anywhere else always sends, so if your beforeExit handler starts more work, such as the next job in a queue, await SdkGarden.flush() at the end of each job.
Serverless functions and edge workers can be frozen as soon as a handler returns, before the timer fires. await SdkGarden.flush() before returning; in Cloudflare Workers, pass it to ctx.waitUntil().
SdkGarden._flush() from 0.1.0 still works: it starts sending like flush() but returns nothing to wait on.
Guarantees
Silent failure
All network errors in the flush path are caught and discarded. SDK Garden will never throw, reject, or surface a console error to your users due to a failed HTTP request, and flush() and shutdown() never reject. Events that fail to deliver are dropped — there is no retry in v1. An event whose properties cannot be serialized to JSON (a BigInt, a circular object) is dropped on its own.
Bounded resources
Routine sends go out one request at a time, and at most 1,000 events wait to be sent; when the queue is full, newer events are dropped. A request times out after 2 seconds in Node.js and 10 seconds in browsers. After a failed request, the rest of the queue waits for the next timer tick instead of going out at once. flush() and the sends at exit or page hide send up to 10 requests at once, combining up to 100 events in each when the queue is long; shutdown() sends everything queued.
An event larger than 64 KiB once serialized is dropped on its own, error messages are cut to 1,000 UTF-8 bytes (as much as the server keeps), and no request carries more than 512 KiB, so one oversized event never sinks the rest of its batch. For the same reason NUL characters and lone surrogates, which the server cannot store, are sent as U+FFFD, and an event whose name is not a string of at most 100 characters is dropped.
API key security
The API key is never logged, even when debug: true. Debug output replaces the key with ***REDACTED***.
No interference
trackMethod preserves the original function's return type, this binding, and all arguments. Errors thrown by your function are re-thrown unchanged after being recorded, and a failure inside the instrumentation never changes what your function returns or throws.
One client per process
sdk-garden-js is a single client shared by everything in the process that imports it. If two SDKs in the same app both use it and npm installs one copy for both, they share that client: the last init() decides the API key, SDK name and opt-out for both. A client per SDK is planned; until then, bundling sdk-garden-js into your SDK's build gives your SDK its own copy.
Usage in test environments
SdkGarden.init({
apiKey: 'sgk_test',
sdkName: 'my-sdk',
sdkVersion: '0.0.0',
disabled: process.env.NODE_ENV === 'test',
});
When disabled: true:
- No flush timer is started.
- No network calls are made.
- Functions wrapped with
trackMethodcall straight through to the original. - All other methods are no-ops.
TypeScript support
Full type definitions are included in the package. Two types are exported for use in your own code:
SdkGardenConfig— the configuration object passed toinit().SdkMethodDecorator— the return type of the single-argumenttrackMethod(name)overload. Use this when you want to type a helper that returns a decorator.
import SdkGarden, { type SdkGardenConfig, type SdkMethodDecorator } from 'sdk-garden-js';
function initObservability(config: Pick<SdkGardenConfig, 'apiKey' | 'sdkName' | 'sdkVersion'>) {
SdkGarden.init(config);
}
function track(name: string): SdkMethodDecorator {
return SdkGarden.trackMethod(name);
}