@stimpact/sdk 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 ADDED
@@ -0,0 +1,102 @@
1
+ # Stimpact SDK
2
+
3
+ The TypeScript SDK sends runtime errors and heartbeats to the Stimpact agent platform.
4
+
5
+ Server runtimes should use a private project `apiKey`.
6
+
7
+ Browser runtimes should use either:
8
+
9
+ - a short-lived token flow via `browserKey`
10
+ - a custom `browserTokenEndpoint`
11
+ - a custom `tokenProvider`
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install @stimpact/sdk
17
+ ```
18
+
19
+ ## Server usage
20
+
21
+ ```ts
22
+ import { StimpactClient } from "@stimpact/sdk";
23
+
24
+ const stimpact = new StimpactClient({
25
+ baseUrl: "https://your-stimpact-api.example.com",
26
+ projectId: "billing-prod",
27
+ apiKey: "stimp_live_...",
28
+ service: "billing-api",
29
+ environment: "production",
30
+ });
31
+
32
+ try {
33
+ await doWork();
34
+ } catch (error) {
35
+ await stimpact.captureError({
36
+ error,
37
+ request: {
38
+ method: "POST",
39
+ url: "/api/charge",
40
+ },
41
+ });
42
+ }
43
+ ```
44
+
45
+ ## Browser usage
46
+
47
+ ```ts
48
+ import { StimpactClient } from "@stimpact/sdk";
49
+
50
+ const stimpact = new StimpactClient({
51
+ baseUrl: "https://your-stimpact-api.example.com",
52
+ projectId: "billing-prod",
53
+ browserKey: "stimp_browser_...",
54
+ service: "billing-web",
55
+ environment: "production",
56
+ });
57
+
58
+ stimpact.startHeartbeat();
59
+ stimpact.registerBrowserAutoCapture();
60
+ ```
61
+
62
+ For the strongest separation, use `browserTokenEndpoint` or `tokenProvider` so your app backend can mint short-lived ingest tokens without exposing any long-lived server credential to the browser.
63
+
64
+ ## Browser autocapture
65
+
66
+ ```ts
67
+ const subscription = stimpact.registerBrowserAutoCapture();
68
+
69
+ // Later, if needed:
70
+ subscription.dispose();
71
+ ```
72
+
73
+ ## Wrapped async flows
74
+
75
+ ```ts
76
+ await stimpact.wrapAsync(async () => {
77
+ await saveInvoice();
78
+ });
79
+ ```
80
+
81
+ ## Data minimization defaults
82
+
83
+ By default the SDK:
84
+
85
+ - sends the normalized error message and stacktrace
86
+ - does not forward request or response context unless you explicitly opt in
87
+ - redacts common sensitive headers such as `authorization`, `cookie`, `set-cookie`, and `x-api-key`
88
+ - omits request and response bodies unless `includeBodies` is enabled
89
+
90
+ To opt in to richer HTTP context:
91
+
92
+ ```ts
93
+ const stimpact = new StimpactClient({
94
+ baseUrl: "https://your-stimpact-api.example.com",
95
+ projectId: "billing-prod",
96
+ apiKey: "stimp_live_...",
97
+ service: "billing-api",
98
+ captureRequestContext: true,
99
+ captureResponseContext: true,
100
+ includeBodies: false,
101
+ });
102
+ ```
@@ -0,0 +1,33 @@
1
+ import type { BrowserAutoCaptureOptions, BrowserCaptureSubscription, CaptureErrorInput, HeartbeatInput, HeartbeatSubscription, StimpactClientOptions } from "./types.js";
2
+ export declare class StimpactRequestError extends Error {
3
+ status: number | null;
4
+ retryable: boolean;
5
+ responseBody: string | null;
6
+ constructor(message: string, options?: {
7
+ status?: number | null;
8
+ retryable?: boolean;
9
+ responseBody?: string | null;
10
+ });
11
+ }
12
+ export declare class StimpactClient {
13
+ private readonly options;
14
+ private cachedBrowserToken;
15
+ private cachedBrowserTokenExpiresAtMs;
16
+ private browserTokenPromise;
17
+ constructor(options: StimpactClientOptions);
18
+ captureError(input: CaptureErrorInput): Promise<void>;
19
+ sendHeartbeat(input?: HeartbeatInput): Promise<void>;
20
+ startHeartbeat(options?: HeartbeatInput & {
21
+ intervalMs?: number;
22
+ }): HeartbeatSubscription;
23
+ wrapAsync<T>(operation: () => Promise<T>, context?: Omit<CaptureErrorInput, "error">): Promise<T>;
24
+ registerBrowserAutoCapture(options?: BrowserAutoCaptureOptions): BrowserCaptureSubscription;
25
+ private sendTelemetry;
26
+ private sendHeartbeatPayload;
27
+ private sendTelemetryOnce;
28
+ private sendJson;
29
+ private buildAuthHeaders;
30
+ private getBrowserToken;
31
+ private fetchBrowserToken;
32
+ private performJsonRequest;
33
+ }
package/dist/client.js ADDED
@@ -0,0 +1,487 @@
1
+ const DEFAULT_REDACTED_HEADERS = new Set([
2
+ "authorization",
3
+ "cookie",
4
+ "set-cookie",
5
+ "proxy-authorization",
6
+ "x-api-key",
7
+ "x-stimpact-project-key",
8
+ ]);
9
+ export class StimpactRequestError extends Error {
10
+ status;
11
+ retryable;
12
+ responseBody;
13
+ constructor(message, options = {}) {
14
+ super(message);
15
+ this.name = "StimpactRequestError";
16
+ this.status = options.status ?? null;
17
+ this.retryable = options.retryable ?? false;
18
+ this.responseBody = options.responseBody ?? null;
19
+ }
20
+ }
21
+ export class StimpactClient {
22
+ options;
23
+ cachedBrowserToken = null;
24
+ cachedBrowserTokenExpiresAtMs = 0;
25
+ browserTokenPromise = null;
26
+ constructor(options) {
27
+ if (!options.apiKey && !options.browserKey && !options.browserTokenEndpoint && !options.tokenProvider) {
28
+ throw new Error("StimpactClient requires apiKey, browserKey, browserTokenEndpoint, or tokenProvider.");
29
+ }
30
+ this.options = {
31
+ ...options,
32
+ baseUrl: options.baseUrl.replace(/\/$/, ""),
33
+ environment: options.environment ?? "production",
34
+ fetchImpl: options.fetchImpl ?? fetch,
35
+ headers: options.headers ?? {},
36
+ timeoutMs: options.timeoutMs ?? 5_000,
37
+ retryAttempts: options.retryAttempts ?? 2,
38
+ retryDelayMs: options.retryDelayMs ?? 250,
39
+ captureRequestContext: options.captureRequestContext ?? false,
40
+ captureResponseContext: options.captureResponseContext ?? false,
41
+ includeBodies: options.includeBodies ?? false,
42
+ redactedHeaders: options.redactedHeaders ?? [],
43
+ maxValueLength: options.maxValueLength ?? 2_048,
44
+ };
45
+ }
46
+ async captureError(input) {
47
+ const normalized = normalizeError(input.error, this.options.maxValueLength);
48
+ const payload = {
49
+ project_id: this.options.projectId,
50
+ environment: input.environment ?? this.options.environment ?? "production",
51
+ service: input.service ?? this.options.service,
52
+ error_message: normalized.message,
53
+ stacktrace: normalized.stacktrace,
54
+ request: this.options.captureRequestContext
55
+ ? sanitizeRequestContext(input.request, this.options)
56
+ : undefined,
57
+ response: this.options.captureResponseContext
58
+ ? sanitizeResponseContext(input.response, this.options)
59
+ : undefined,
60
+ commit_sha: input.commitSha ?? null,
61
+ timestamp: normalizeTimestamp(input.timestamp),
62
+ };
63
+ await this.sendTelemetry(payload);
64
+ }
65
+ async sendHeartbeat(input = {}) {
66
+ const payload = {
67
+ project_id: this.options.projectId,
68
+ environment: input.environment ?? this.options.environment ?? "production",
69
+ service: input.service ?? this.options.service,
70
+ commit_sha: input.commitSha ?? null,
71
+ timestamp: normalizeTimestamp(input.timestamp),
72
+ };
73
+ await this.sendHeartbeatPayload(payload);
74
+ }
75
+ startHeartbeat(options = {}) {
76
+ const intervalMs = options.intervalMs ?? 300_000;
77
+ let intervalId = null;
78
+ void this.sendHeartbeat(options);
79
+ if (typeof setInterval !== "undefined") {
80
+ intervalId = setInterval(() => {
81
+ void this.sendHeartbeat(options);
82
+ }, intervalMs);
83
+ }
84
+ return {
85
+ dispose: () => {
86
+ if (intervalId !== null && typeof clearInterval !== "undefined") {
87
+ clearInterval(intervalId);
88
+ }
89
+ },
90
+ };
91
+ }
92
+ async wrapAsync(operation, context) {
93
+ try {
94
+ return await operation();
95
+ }
96
+ catch (error) {
97
+ try {
98
+ await this.captureError({
99
+ ...context,
100
+ error,
101
+ });
102
+ }
103
+ catch (captureFailure) {
104
+ if (error instanceof Error) {
105
+ Object.defineProperty(error, "captureFailure", {
106
+ value: captureFailure,
107
+ configurable: true,
108
+ });
109
+ }
110
+ }
111
+ throw error;
112
+ }
113
+ }
114
+ registerBrowserAutoCapture(options = {}) {
115
+ if (typeof window === "undefined") {
116
+ return { dispose: () => undefined };
117
+ }
118
+ const captureWindowErrors = options.captureWindowErrors ?? true;
119
+ const captureUnhandledRejections = options.captureUnhandledRejections ?? true;
120
+ const errorListener = (event) => {
121
+ void this.captureError({
122
+ error: event.error ?? event.message,
123
+ service: this.options.service,
124
+ });
125
+ };
126
+ const rejectionListener = (event) => {
127
+ void this.captureError({
128
+ error: event.reason ?? "Unhandled promise rejection",
129
+ service: this.options.service,
130
+ });
131
+ };
132
+ if (captureWindowErrors) {
133
+ window.addEventListener("error", errorListener);
134
+ }
135
+ if (captureUnhandledRejections) {
136
+ window.addEventListener("unhandledrejection", rejectionListener);
137
+ }
138
+ return {
139
+ dispose: () => {
140
+ if (captureWindowErrors) {
141
+ window.removeEventListener("error", errorListener);
142
+ }
143
+ if (captureUnhandledRejections) {
144
+ window.removeEventListener("unhandledrejection", rejectionListener);
145
+ }
146
+ },
147
+ };
148
+ }
149
+ async sendTelemetry(payload) {
150
+ let lastError;
151
+ for (let attempt = 0; attempt <= this.options.retryAttempts; attempt += 1) {
152
+ try {
153
+ await this.sendTelemetryOnce(payload);
154
+ return;
155
+ }
156
+ catch (error) {
157
+ lastError = error;
158
+ if (!(error instanceof StimpactRequestError) || !error.retryable || attempt === this.options.retryAttempts) {
159
+ throw error;
160
+ }
161
+ await delay(this.options.retryDelayMs * (attempt + 1));
162
+ }
163
+ }
164
+ throw lastError instanceof Error
165
+ ? lastError
166
+ : new StimpactRequestError("Telemetry delivery failed.");
167
+ }
168
+ async sendHeartbeatPayload(payload) {
169
+ let lastError;
170
+ for (let attempt = 0; attempt <= this.options.retryAttempts; attempt += 1) {
171
+ try {
172
+ await this.sendJson(`${this.options.baseUrl}/telemetry/heartbeat`, payload);
173
+ return;
174
+ }
175
+ catch (error) {
176
+ lastError = error;
177
+ if (!(error instanceof StimpactRequestError) || !error.retryable || attempt === this.options.retryAttempts) {
178
+ throw error;
179
+ }
180
+ await delay(this.options.retryDelayMs * (attempt + 1));
181
+ }
182
+ }
183
+ throw lastError instanceof Error
184
+ ? lastError
185
+ : new StimpactRequestError("Telemetry heartbeat delivery failed.");
186
+ }
187
+ async sendTelemetryOnce(payload) {
188
+ await this.sendJson(`${this.options.baseUrl}/telemetry/error`, payload);
189
+ }
190
+ async sendJson(url, payload) {
191
+ const fetchImpl = this.options.fetchImpl ?? fetch;
192
+ const controller = typeof AbortController === "undefined" ? null : new AbortController();
193
+ const timeout = controller
194
+ ? setTimeout(() => controller.abort(), this.options.timeoutMs)
195
+ : null;
196
+ try {
197
+ const authHeaders = await this.buildAuthHeaders(payload);
198
+ const response = await fetchImpl(url, {
199
+ method: "POST",
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ ...authHeaders,
203
+ ...this.options.headers,
204
+ },
205
+ body: JSON.stringify(payload),
206
+ signal: controller?.signal,
207
+ });
208
+ if (!response.ok) {
209
+ const responseBody = await safeReadResponseText(response);
210
+ throw new StimpactRequestError(`Telemetry delivery failed with status ${response.status}.`, {
211
+ status: response.status,
212
+ retryable: response.status >= 500 || response.status === 429,
213
+ responseBody,
214
+ });
215
+ }
216
+ }
217
+ catch (error) {
218
+ if (error instanceof StimpactRequestError) {
219
+ throw error;
220
+ }
221
+ throw new StimpactRequestError("Telemetry delivery failed before the platform acknowledged it.", {
222
+ retryable: true,
223
+ });
224
+ }
225
+ finally {
226
+ if (timeout !== null) {
227
+ clearTimeout(timeout);
228
+ }
229
+ }
230
+ }
231
+ async buildAuthHeaders(payload) {
232
+ if (this.options.apiKey) {
233
+ return {
234
+ "X-Stimpact-Project-Key": this.options.apiKey,
235
+ };
236
+ }
237
+ const token = await this.getBrowserToken(payload);
238
+ return {
239
+ Authorization: `Bearer ${token}`,
240
+ };
241
+ }
242
+ async getBrowserToken(payload) {
243
+ if (this.options.tokenProvider) {
244
+ return this.options.tokenProvider();
245
+ }
246
+ const refreshThresholdMs = 15_000;
247
+ if (this.cachedBrowserToken &&
248
+ Date.now() + refreshThresholdMs < this.cachedBrowserTokenExpiresAtMs) {
249
+ return this.cachedBrowserToken;
250
+ }
251
+ if (this.browserTokenPromise) {
252
+ return this.browserTokenPromise;
253
+ }
254
+ this.browserTokenPromise = this.fetchBrowserToken(payload).finally(() => {
255
+ this.browserTokenPromise = null;
256
+ });
257
+ return this.browserTokenPromise;
258
+ }
259
+ async fetchBrowserToken(payload) {
260
+ const endpoint = this.options.browserTokenEndpoint ??
261
+ `${this.options.baseUrl}/telemetry/browser-token`;
262
+ const body = {
263
+ project_id: this.options.projectId,
264
+ browser_key: this.options.browserKey,
265
+ service: payload.service,
266
+ environment: payload.environment,
267
+ };
268
+ const response = await this.performJsonRequest(endpoint, body);
269
+ const parsed = (await safeReadResponseJson(response));
270
+ const token = typeof parsed?.token === "string" ? parsed.token : null;
271
+ if (!token) {
272
+ throw new StimpactRequestError("Browser token request succeeded without a token payload.");
273
+ }
274
+ const expiresAtMs = resolveTokenExpiryMs(parsed);
275
+ this.cachedBrowserToken = token;
276
+ this.cachedBrowserTokenExpiresAtMs = expiresAtMs;
277
+ return token;
278
+ }
279
+ async performJsonRequest(url, payload) {
280
+ const fetchImpl = this.options.fetchImpl ?? fetch;
281
+ const controller = typeof AbortController === "undefined" ? null : new AbortController();
282
+ const timeout = controller
283
+ ? setTimeout(() => controller.abort(), this.options.timeoutMs)
284
+ : null;
285
+ try {
286
+ const response = await fetchImpl(url, {
287
+ method: "POST",
288
+ headers: {
289
+ "Content-Type": "application/json",
290
+ ...this.options.headers,
291
+ },
292
+ body: JSON.stringify(payload),
293
+ signal: controller?.signal,
294
+ });
295
+ if (!response.ok) {
296
+ const responseBody = await safeReadResponseText(response);
297
+ throw new StimpactRequestError(`Request failed with status ${response.status}.`, {
298
+ status: response.status,
299
+ retryable: response.status >= 500 || response.status === 429,
300
+ responseBody,
301
+ });
302
+ }
303
+ return response;
304
+ }
305
+ catch (error) {
306
+ if (error instanceof StimpactRequestError) {
307
+ throw error;
308
+ }
309
+ throw new StimpactRequestError("Request failed before the platform acknowledged it.", {
310
+ retryable: true,
311
+ });
312
+ }
313
+ finally {
314
+ if (timeout !== null) {
315
+ clearTimeout(timeout);
316
+ }
317
+ }
318
+ }
319
+ }
320
+ function normalizeTimestamp(value) {
321
+ if (!value) {
322
+ return new Date().toISOString();
323
+ }
324
+ return value instanceof Date ? value.toISOString() : value;
325
+ }
326
+ function normalizeError(error, maxValueLength) {
327
+ if (error instanceof Error) {
328
+ return {
329
+ message: clampString(error.message || error.name || "Unknown error", maxValueLength),
330
+ stacktrace: clampString(error.stack || error.message || error.name, maxValueLength * 4),
331
+ };
332
+ }
333
+ if (typeof error === "string") {
334
+ return {
335
+ message: clampString(error, maxValueLength),
336
+ stacktrace: clampString(error, maxValueLength * 4),
337
+ };
338
+ }
339
+ const sanitized = sanitizeUnknown(error, {
340
+ includeBodies: false,
341
+ maxValueLength,
342
+ redactedHeaders: [],
343
+ });
344
+ const serialized = safeSerialize(sanitized);
345
+ return {
346
+ message: typeof sanitized === "object" &&
347
+ sanitized !== null &&
348
+ "message" in sanitized &&
349
+ typeof sanitized.message === "string"
350
+ ? clampString(sanitized.message, maxValueLength)
351
+ : "Unknown error",
352
+ stacktrace: clampString(serialized, maxValueLength * 4),
353
+ };
354
+ }
355
+ async function safeReadResponseText(response) {
356
+ try {
357
+ const text = await response.text();
358
+ return text || null;
359
+ }
360
+ catch {
361
+ return null;
362
+ }
363
+ }
364
+ async function safeReadResponseJson(response) {
365
+ try {
366
+ return await response.json();
367
+ }
368
+ catch {
369
+ return null;
370
+ }
371
+ }
372
+ function sanitizeRequestContext(request, options) {
373
+ if (!request) {
374
+ return undefined;
375
+ }
376
+ return omitUndefined({
377
+ method: request.method
378
+ ? clampString(request.method, 32)
379
+ : undefined,
380
+ url: request.url
381
+ ? clampString(request.url, options.maxValueLength ?? 2_048)
382
+ : undefined,
383
+ headers: sanitizeHeaders(request.headers, options.redactedHeaders, options.maxValueLength),
384
+ body: options.includeBodies
385
+ ? sanitizeUnknown(request.body, options)
386
+ : undefined,
387
+ });
388
+ }
389
+ function sanitizeResponseContext(response, options) {
390
+ if (!response) {
391
+ return undefined;
392
+ }
393
+ return omitUndefined({
394
+ status_code: response.status_code,
395
+ headers: sanitizeHeaders(response.headers, options.redactedHeaders, options.maxValueLength),
396
+ body: options.includeBodies
397
+ ? sanitizeUnknown(response.body, options)
398
+ : undefined,
399
+ });
400
+ }
401
+ function sanitizeHeaders(headers, redactedHeaders, maxValueLength = 2_048) {
402
+ if (!headers) {
403
+ return undefined;
404
+ }
405
+ const extraRedactions = new Set((redactedHeaders ?? []).map((value) => value.toLowerCase()));
406
+ const entries = Object.entries(headers)
407
+ .slice(0, 40)
408
+ .map(([key, value]) => {
409
+ const lowerKey = key.toLowerCase();
410
+ const shouldRedact = DEFAULT_REDACTED_HEADERS.has(lowerKey) || extraRedactions.has(lowerKey);
411
+ return [key, shouldRedact ? "[REDACTED]" : clampString(String(value), maxValueLength)];
412
+ });
413
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
414
+ }
415
+ function sanitizeUnknown(value, options, seen = new WeakSet(), depth = 0) {
416
+ const maxValueLength = options.maxValueLength ?? 2_048;
417
+ if (value == null || typeof value === "number" || typeof value === "boolean") {
418
+ return value;
419
+ }
420
+ if (typeof value === "string") {
421
+ return clampString(value, maxValueLength);
422
+ }
423
+ if (typeof value === "bigint") {
424
+ return clampString(value.toString(), maxValueLength);
425
+ }
426
+ if (typeof value === "function" || typeof value === "symbol") {
427
+ return clampString(String(value), maxValueLength);
428
+ }
429
+ if (depth >= 4) {
430
+ return "[TRUNCATED]";
431
+ }
432
+ if (Array.isArray(value)) {
433
+ return value
434
+ .slice(0, 20)
435
+ .map((item) => sanitizeUnknown(item, options, seen, depth + 1));
436
+ }
437
+ if (typeof value === "object") {
438
+ if (seen.has(value)) {
439
+ return "[Circular]";
440
+ }
441
+ seen.add(value);
442
+ const output = {};
443
+ for (const [key, entry] of Object.entries(value).slice(0, 25)) {
444
+ output[key] = sanitizeUnknown(entry, options, seen, depth + 1);
445
+ }
446
+ return output;
447
+ }
448
+ return clampString(String(value), maxValueLength);
449
+ }
450
+ function safeSerialize(value) {
451
+ try {
452
+ return JSON.stringify(value, null, 2);
453
+ }
454
+ catch {
455
+ return String(value);
456
+ }
457
+ }
458
+ function clampString(value, maxLength) {
459
+ if (value.length <= maxLength) {
460
+ return value;
461
+ }
462
+ return `${value.slice(0, maxLength)}...[truncated]`;
463
+ }
464
+ function omitUndefined(value) {
465
+ const entries = Object.entries(value).filter(([, entry]) => entry !== undefined);
466
+ if (entries.length === 0) {
467
+ return undefined;
468
+ }
469
+ return Object.fromEntries(entries);
470
+ }
471
+ function resolveTokenExpiryMs(response) {
472
+ if (typeof response?.expires_at === "string") {
473
+ const parsed = Date.parse(response.expires_at);
474
+ if (!Number.isNaN(parsed)) {
475
+ return parsed;
476
+ }
477
+ }
478
+ if (typeof response?.expires_in_seconds === "number" && Number.isFinite(response.expires_in_seconds)) {
479
+ return Date.now() + Math.max(30, response.expires_in_seconds) * 1_000;
480
+ }
481
+ return Date.now() + 60_000;
482
+ }
483
+ function delay(ms) {
484
+ return new Promise((resolve) => {
485
+ setTimeout(resolve, ms);
486
+ });
487
+ }
@@ -0,0 +1,2 @@
1
+ export { StimpactClient, StimpactRequestError } from "./client.js";
2
+ export type { BrowserAutoCaptureOptions, BrowserCaptureSubscription, CaptureErrorInput, HeartbeatInput, HeartbeatSubscription, HttpRequestContext, HttpResponseContext, StimpactClientOptions, StimpactEnvironment, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { StimpactClient, StimpactRequestError } from "./client.js";
@@ -0,0 +1,58 @@
1
+ export type StimpactEnvironment = "production" | "staging" | "development" | "test";
2
+ export type StimpactTokenProvider = () => Promise<string>;
3
+ export type HttpRequestContext = {
4
+ method?: string;
5
+ url?: string;
6
+ headers?: Record<string, string>;
7
+ body?: unknown;
8
+ };
9
+ export type HttpResponseContext = {
10
+ status_code?: number;
11
+ headers?: Record<string, string>;
12
+ body?: unknown;
13
+ };
14
+ export type CaptureErrorInput = {
15
+ error: unknown;
16
+ request?: HttpRequestContext;
17
+ response?: HttpResponseContext;
18
+ commitSha?: string | null;
19
+ environment?: StimpactEnvironment;
20
+ service?: string;
21
+ timestamp?: string | Date;
22
+ };
23
+ export type HeartbeatInput = {
24
+ commitSha?: string | null;
25
+ environment?: StimpactEnvironment;
26
+ service?: string;
27
+ timestamp?: string | Date;
28
+ };
29
+ export type StimpactClientOptions = {
30
+ baseUrl: string;
31
+ projectId: string;
32
+ apiKey?: string;
33
+ browserKey?: string;
34
+ browserTokenEndpoint?: string;
35
+ tokenProvider?: StimpactTokenProvider;
36
+ service: string;
37
+ environment?: StimpactEnvironment;
38
+ fetchImpl?: typeof fetch;
39
+ headers?: Record<string, string>;
40
+ timeoutMs?: number;
41
+ retryAttempts?: number;
42
+ retryDelayMs?: number;
43
+ captureRequestContext?: boolean;
44
+ captureResponseContext?: boolean;
45
+ includeBodies?: boolean;
46
+ redactedHeaders?: string[];
47
+ maxValueLength?: number;
48
+ };
49
+ export type BrowserAutoCaptureOptions = {
50
+ captureWindowErrors?: boolean;
51
+ captureUnhandledRejections?: boolean;
52
+ };
53
+ export type BrowserCaptureSubscription = {
54
+ dispose: () => void;
55
+ };
56
+ export type HeartbeatSubscription = {
57
+ dispose: () => void;
58
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@stimpact/sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.json",
12
+ "test": "npm run build && node --test tests/**/*.test.mjs"
13
+ },
14
+ "devDependencies": {
15
+ "typescript": "^5.9.3"
16
+ }
17
+ }