@gurulu/node 0.1.2 → 1.0.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/LICENSE +22 -0
- package/README.md +64 -31
- package/dist/context.d.ts +12 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/core.d.ts +60 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/identify.d.ts +9 -0
- package/dist/identify.d.ts.map +1 -0
- package/dist/index.d.ts +13 -13
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +947 -28
- package/dist/middleware/express.d.ts +25 -0
- package/dist/middleware/express.d.ts.map +1 -0
- package/dist/middleware/express.js +66 -0
- package/dist/middleware/fastify.d.ts +20 -0
- package/dist/middleware/fastify.d.ts.map +1 -0
- package/dist/middleware/fastify.js +68 -0
- package/dist/middleware/next.d.ts +10 -0
- package/dist/middleware/next.d.ts.map +1 -0
- package/dist/middleware/next.js +69 -0
- package/dist/queue.d.ts +20 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/track.d.ts +10 -0
- package/dist/track.d.ts.map +1 -0
- package/dist/transport.d.ts +16 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/types.d.ts +129 -45
- package/dist/types.d.ts.map +1 -0
- package/dist/webhooks/custom.d.ts +30 -0
- package/dist/webhooks/custom.d.ts.map +1 -0
- package/dist/webhooks/custom.js +123 -0
- package/dist/webhooks/lemonsqueezy.d.ts +30 -0
- package/dist/webhooks/lemonsqueezy.d.ts.map +1 -0
- package/dist/webhooks/lemonsqueezy.js +140 -0
- package/dist/webhooks/shopify.d.ts +18 -0
- package/dist/webhooks/shopify.d.ts.map +1 -0
- package/dist/webhooks/shopify.js +142 -0
- package/dist/webhooks/stripe.d.ts +31 -0
- package/dist/webhooks/stripe.d.ts.map +1 -0
- package/dist/webhooks/stripe.js +160 -0
- package/package.json +97 -16
- package/dist/business-events.d.ts +0 -73
- package/dist/business-events.js +0 -111
- package/dist/client.d.ts +0 -150
- package/dist/client.js +0 -442
- package/dist/middleware.d.ts +0 -31
- package/dist/middleware.js +0 -138
- package/dist/types.js +0 -30
package/dist/client.js
DELETED
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @gurulu/node — server-side SDK client.
|
|
4
|
-
*
|
|
5
|
-
* Responsibilities (Phase 10 W4.2):
|
|
6
|
-
* - Queue + batch server events.
|
|
7
|
-
* - Auto-flush at batchSize boundary and on a periodic timer.
|
|
8
|
-
* - Exponential-backoff retry (200ms, 600ms, 1800ms — 3 attempts total) on
|
|
9
|
-
* 5xx / network errors. No retry on 4xx.
|
|
10
|
-
* - Attach deterministic Idempotency-Key headers so the server can dedupe
|
|
11
|
-
* replayed batches across retries.
|
|
12
|
-
* - Dead-letter callback for batches that exhaust retries or are rejected
|
|
13
|
-
* with a client error.
|
|
14
|
-
* - Flush on process.beforeExit + SIGTERM.
|
|
15
|
-
*
|
|
16
|
-
* Event shapes are pulled from @gurulu/shared-core (W1.2). Only the transport
|
|
17
|
-
* config (`GuruluClientConfig`) is local.
|
|
18
|
-
*
|
|
19
|
-
* Related docs: PHASE-10-ROADMAP.md §W4.2
|
|
20
|
-
*/
|
|
21
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.Gurulu = void 0;
|
|
23
|
-
exports.createIdempotencyKey = createIdempotencyKey;
|
|
24
|
-
exports.setDefaultInstance = setDefaultInstance;
|
|
25
|
-
exports.captureException = captureException;
|
|
26
|
-
exports.addBreadcrumb = addBreadcrumb;
|
|
27
|
-
const crypto_1 = require("crypto");
|
|
28
|
-
const DEFAULT_ENDPOINT = 'https://app.gurulu.io/api/ingest/v1/server';
|
|
29
|
-
const SDK_VERSION = 'node@0.1.0';
|
|
30
|
-
const DEFAULT_FLUSH_INTERVAL = 5000;
|
|
31
|
-
const DEFAULT_BATCH_SIZE = 50;
|
|
32
|
-
const DEFAULT_TIMEOUT = 10000;
|
|
33
|
-
const RETRY_DELAYS_MS = [200, 600, 1800];
|
|
34
|
-
/**
|
|
35
|
-
* Deterministic idempotency key for a server event. Hashes the tuple
|
|
36
|
-
* (site_id, event_name, timestamp, anonymous_id/user_id) so replayed events
|
|
37
|
-
* across retries produce the same key and the server can dedupe.
|
|
38
|
-
*/
|
|
39
|
-
function createIdempotencyKey(event, siteId) {
|
|
40
|
-
const parts = [
|
|
41
|
-
siteId,
|
|
42
|
-
event.event_name ?? '',
|
|
43
|
-
event.timestamp ?? '',
|
|
44
|
-
event.user_id ?? '',
|
|
45
|
-
];
|
|
46
|
-
return (0, crypto_1.createHash)('sha256').update(parts.join('|')).digest('hex');
|
|
47
|
-
}
|
|
48
|
-
class Gurulu {
|
|
49
|
-
siteId;
|
|
50
|
-
apiKey;
|
|
51
|
-
endpoint;
|
|
52
|
-
flushInterval;
|
|
53
|
-
batchSize;
|
|
54
|
-
timeout;
|
|
55
|
-
onError;
|
|
56
|
-
onDeadLetter;
|
|
57
|
-
fetchImpl;
|
|
58
|
-
debug;
|
|
59
|
-
release;
|
|
60
|
-
queue = [];
|
|
61
|
-
breadcrumbs = [];
|
|
62
|
-
maxBreadcrumbs = 50;
|
|
63
|
-
flushTimer = null;
|
|
64
|
-
inFlight = null;
|
|
65
|
-
shutdownHandlers = [];
|
|
66
|
-
constructor(config) {
|
|
67
|
-
if (!config.siteId)
|
|
68
|
-
throw new Error('siteId is required');
|
|
69
|
-
if (!config.apiKey)
|
|
70
|
-
throw new Error('apiKey is required');
|
|
71
|
-
this.siteId = config.siteId;
|
|
72
|
-
this.apiKey = config.apiKey;
|
|
73
|
-
this.endpoint = config.endpoint || DEFAULT_ENDPOINT;
|
|
74
|
-
this.flushInterval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
75
|
-
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
76
|
-
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
77
|
-
this.onError = config.onError;
|
|
78
|
-
this.onDeadLetter = config.onDeadLetter;
|
|
79
|
-
this.debug = config.debug ?? false;
|
|
80
|
-
this.release = config.release || process.env.GURULU_RELEASE || '';
|
|
81
|
-
const injected = config.fetchImpl;
|
|
82
|
-
if (injected) {
|
|
83
|
-
this.fetchImpl = injected;
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
const g = globalThis;
|
|
87
|
-
if (typeof g.fetch !== 'function') {
|
|
88
|
-
throw new Error('global fetch is not available; pass fetchImpl in config (Node 18+ required)');
|
|
89
|
-
}
|
|
90
|
-
this.fetchImpl = g.fetch.bind(globalThis);
|
|
91
|
-
}
|
|
92
|
-
if (this.flushInterval > 0) {
|
|
93
|
-
this.flushTimer = setInterval(() => {
|
|
94
|
-
// Fire and forget; errors surface via onError.
|
|
95
|
-
void this.flush();
|
|
96
|
-
}, this.flushInterval);
|
|
97
|
-
// Let the process exit even if the timer is pending.
|
|
98
|
-
const t = this.flushTimer;
|
|
99
|
-
if (typeof t.unref === 'function')
|
|
100
|
-
t.unref();
|
|
101
|
-
}
|
|
102
|
-
if (!config.disableAutoShutdown && typeof process !== 'undefined' && typeof process.on === 'function') {
|
|
103
|
-
const beforeExit = () => {
|
|
104
|
-
void this.flush();
|
|
105
|
-
};
|
|
106
|
-
const onSigterm = () => {
|
|
107
|
-
void this.shutdown();
|
|
108
|
-
};
|
|
109
|
-
process.on('beforeExit', beforeExit);
|
|
110
|
-
process.on('SIGTERM', onSigterm);
|
|
111
|
-
this.shutdownHandlers.push(() => {
|
|
112
|
-
process.off('beforeExit', beforeExit);
|
|
113
|
-
process.off('SIGTERM', onSigterm);
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
get queueSize() {
|
|
118
|
-
return this.queue.length;
|
|
119
|
-
}
|
|
120
|
-
static REVENUE_EVENTS = new Set([
|
|
121
|
-
'purchase', '$purchase', 'order_placed', '$order_placed',
|
|
122
|
-
'subscription_started', '$subscription_started', 'subscription_created', '$subscription_created',
|
|
123
|
-
'refund', '$refund', 'order_refunded', '$order_refunded',
|
|
124
|
-
'payment_succeeded', '$payment_succeeded', 'checkout_completed', '$checkout_completed',
|
|
125
|
-
'deposit_completed', '$deposit_completed', 'bet_placed', '$bet_placed',
|
|
126
|
-
'subscribe', '$subscribe',
|
|
127
|
-
]);
|
|
128
|
-
/**
|
|
129
|
-
* Enqueue a server event. Auto-flushes synchronously when the queue reaches
|
|
130
|
-
* `batchSize`. Each event is stamped with a timestamp if missing so the
|
|
131
|
-
* idempotency key is stable across retries.
|
|
132
|
-
*/
|
|
133
|
-
track(event) {
|
|
134
|
-
if (!event || !event.event_name) {
|
|
135
|
-
if (this.debug)
|
|
136
|
-
console.warn('[gurulu] event_name is required');
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (!event.user_id) {
|
|
140
|
-
if (this.debug)
|
|
141
|
-
console.warn('[gurulu] user_id is required for server events');
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
// Warn when a known revenue event is missing value/amount or currency
|
|
145
|
-
const eName = event.event_name;
|
|
146
|
-
if (Gurulu.REVENUE_EVENTS.has(eName) ||
|
|
147
|
-
Gurulu.REVENUE_EVENTS.has(eName.replace(/^\$/, ''))) {
|
|
148
|
-
const props = event.properties;
|
|
149
|
-
const hasValue = props?.value !== undefined || props?.amount !== undefined;
|
|
150
|
-
if (!hasValue) {
|
|
151
|
-
console.warn(`[gurulu] Revenue event "${eName}" is missing "value"/"amount" property. Revenue tracking will be incomplete.`);
|
|
152
|
-
}
|
|
153
|
-
if (!props?.currency) {
|
|
154
|
-
console.warn(`[gurulu] Revenue event "${eName}" is missing "currency" property (expected ISO 4217 code like "USD", "EUR").`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const stamped = {
|
|
158
|
-
...event,
|
|
159
|
-
timestamp: event.timestamp ?? new Date().toISOString(),
|
|
160
|
-
};
|
|
161
|
-
this.queue.push(stamped);
|
|
162
|
-
if (this.debug) {
|
|
163
|
-
console.log(`[gurulu] queued: ${stamped.event_name} (${this.queue.length}/${this.batchSize})`);
|
|
164
|
-
}
|
|
165
|
-
if (this.queue.length >= this.batchSize) {
|
|
166
|
-
void this.flush();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Identify a user — links an anonymous session to a known user ID and
|
|
171
|
-
* persists traits on the identity profile. Sends immediately to the
|
|
172
|
-
* dedicated `/api/ingest/v1/identify` endpoint (not batched with track
|
|
173
|
-
* events) because identity resolution is latency-sensitive and returns
|
|
174
|
-
* the canonical_id synchronously.
|
|
175
|
-
*
|
|
176
|
-
* Retries with the same exponential-backoff strategy as batch flushes.
|
|
177
|
-
*/
|
|
178
|
-
async identify(params) {
|
|
179
|
-
if (!params.userId)
|
|
180
|
-
throw new Error('userId is required for identify()');
|
|
181
|
-
if (!params.anonymousId)
|
|
182
|
-
throw new Error('anonymousId is required for identify()');
|
|
183
|
-
const identifyEndpoint = this.endpoint.replace(/\/server\/?$/, '/identify');
|
|
184
|
-
const body = JSON.stringify({
|
|
185
|
-
site_id: this.siteId,
|
|
186
|
-
anonymous_id: params.anonymousId,
|
|
187
|
-
user_id: params.userId,
|
|
188
|
-
traits: params.traits,
|
|
189
|
-
...(params.deviceId ? { device_id: params.deviceId } : {}),
|
|
190
|
-
...(params.oauthProvider ? { oauth_provider: params.oauthProvider } : {}),
|
|
191
|
-
...(params.oauthId ? { oauth_id: params.oauthId } : {}),
|
|
192
|
-
...(params.consentLevel ? { consent_level: params.consentLevel } : {}),
|
|
193
|
-
});
|
|
194
|
-
const headers = {
|
|
195
|
-
'Content-Type': 'application/json',
|
|
196
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
197
|
-
};
|
|
198
|
-
let lastError = null;
|
|
199
|
-
for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt++) {
|
|
200
|
-
try {
|
|
201
|
-
const res = await this.fetchImpl(identifyEndpoint, {
|
|
202
|
-
method: 'POST',
|
|
203
|
-
headers,
|
|
204
|
-
body,
|
|
205
|
-
});
|
|
206
|
-
if (res.ok) {
|
|
207
|
-
const data = (await res.json());
|
|
208
|
-
if (this.debug)
|
|
209
|
-
console.log(`[gurulu] identify success: canonical_id=${data.canonical_id}`);
|
|
210
|
-
return data;
|
|
211
|
-
}
|
|
212
|
-
if (res.status >= 400 && res.status < 500) {
|
|
213
|
-
const text = await safeText(res);
|
|
214
|
-
const err = new Error(`identify client_error ${res.status}: ${text}`);
|
|
215
|
-
this.onError?.(err);
|
|
216
|
-
throw err;
|
|
217
|
-
}
|
|
218
|
-
lastError = new Error(`identify server_error ${res.status}`);
|
|
219
|
-
this.onError?.(lastError);
|
|
220
|
-
}
|
|
221
|
-
catch (err) {
|
|
222
|
-
if (err instanceof Error && err.message.startsWith('identify client_error')) {
|
|
223
|
-
throw err;
|
|
224
|
-
}
|
|
225
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
226
|
-
this.onError?.(lastError);
|
|
227
|
-
}
|
|
228
|
-
if (attempt < RETRY_DELAYS_MS.length - 1) {
|
|
229
|
-
await sleep(RETRY_DELAYS_MS[attempt]);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
throw lastError ?? new Error('identify failed after retries');
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* POST all queued events in one batch. On 5xx / network error retries with
|
|
236
|
-
* exponential backoff (200ms, 600ms, 1800ms). On 4xx drops the batch and
|
|
237
|
-
* fires the dead-letter callback. Concurrent flush calls are serialized.
|
|
238
|
-
*/
|
|
239
|
-
async flush() {
|
|
240
|
-
// Serialize concurrent flushes so retries don't interleave.
|
|
241
|
-
if (this.inFlight) {
|
|
242
|
-
await this.inFlight;
|
|
243
|
-
}
|
|
244
|
-
if (this.queue.length === 0)
|
|
245
|
-
return;
|
|
246
|
-
const batch = this.queue.splice(0, this.queue.length);
|
|
247
|
-
const run = this.sendBatch(batch);
|
|
248
|
-
this.inFlight = run.finally(() => {
|
|
249
|
-
this.inFlight = null;
|
|
250
|
-
});
|
|
251
|
-
await this.inFlight;
|
|
252
|
-
}
|
|
253
|
-
async sendBatch(batch) {
|
|
254
|
-
// Idempotency key for the whole batch — hashed over individual event keys.
|
|
255
|
-
const perEventKeys = batch.map((e) => createIdempotencyKey(e, this.siteId));
|
|
256
|
-
const batchKey = (0, crypto_1.createHash)('sha256').update(perEventKeys.join('|')).digest('hex');
|
|
257
|
-
const body = JSON.stringify({
|
|
258
|
-
site_id: this.siteId,
|
|
259
|
-
events: batch,
|
|
260
|
-
});
|
|
261
|
-
const headers = {
|
|
262
|
-
'Content-Type': 'application/json',
|
|
263
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
264
|
-
'Idempotency-Key': batchKey,
|
|
265
|
-
};
|
|
266
|
-
let lastError = null;
|
|
267
|
-
for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt++) {
|
|
268
|
-
const controller = new AbortController();
|
|
269
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
270
|
-
try {
|
|
271
|
-
const res = await this.fetchImpl(this.endpoint, {
|
|
272
|
-
method: 'POST',
|
|
273
|
-
headers,
|
|
274
|
-
body,
|
|
275
|
-
signal: controller.signal,
|
|
276
|
-
});
|
|
277
|
-
clearTimeout(timeoutId);
|
|
278
|
-
if (res.ok) {
|
|
279
|
-
if (this.debug)
|
|
280
|
-
console.log(`[gurulu] flushed ${batch.length} events`);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (res.status >= 400 && res.status < 500) {
|
|
284
|
-
// Client error — don't retry. Drop + dead-letter.
|
|
285
|
-
const text = await safeText(res);
|
|
286
|
-
const err = new Error(`client_error ${res.status}: ${text}`);
|
|
287
|
-
if (this.debug)
|
|
288
|
-
console.error(`[gurulu] ${err.message}`);
|
|
289
|
-
this.onError?.(err);
|
|
290
|
-
this.onDeadLetter?.(batch, {
|
|
291
|
-
kind: 'client_error',
|
|
292
|
-
status: res.status,
|
|
293
|
-
message: text,
|
|
294
|
-
});
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
lastError = new Error(`server_error ${res.status}`);
|
|
298
|
-
this.onError?.(lastError);
|
|
299
|
-
}
|
|
300
|
-
catch (err) {
|
|
301
|
-
clearTimeout(timeoutId);
|
|
302
|
-
// Treat AbortError (timeout) as retryable network error
|
|
303
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
304
|
-
lastError = new Error(`timeout after ${this.timeout}ms`);
|
|
305
|
-
this.onError?.(lastError);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
309
|
-
this.onError?.(lastError);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// Not the last attempt — sleep with the fixed exponential schedule.
|
|
313
|
-
if (attempt < RETRY_DELAYS_MS.length - 1) {
|
|
314
|
-
await sleep(RETRY_DELAYS_MS[attempt + 1 - 1]);
|
|
315
|
-
// ^ Index arithmetic: attempt=0 -> use RETRY_DELAYS_MS[0]=200, etc.
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// All 3 attempts failed.
|
|
319
|
-
if (this.debug) {
|
|
320
|
-
console.error(`[gurulu] dropping batch of ${batch.length} after retries: ${lastError?.message}`);
|
|
321
|
-
}
|
|
322
|
-
if (this.onDeadLetter) {
|
|
323
|
-
this.onDeadLetter(batch, {
|
|
324
|
-
kind: lastError?.message.startsWith('server_error') ? 'server_error' : 'network_error',
|
|
325
|
-
message: lastError?.message ?? 'unknown error',
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
else if (this.debug) {
|
|
329
|
-
console.error('[gurulu] no dead-letter callback configured; events lost');
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Add a custom breadcrumb to the trail. Breadcrumbs are sent with error
|
|
334
|
-
* events to provide context about what happened before the error occurred.
|
|
335
|
-
*/
|
|
336
|
-
addBreadcrumb(category, message, data) {
|
|
337
|
-
this.breadcrumbs.push({
|
|
338
|
-
timestamp: new Date().toISOString(),
|
|
339
|
-
category,
|
|
340
|
-
message,
|
|
341
|
-
data,
|
|
342
|
-
});
|
|
343
|
-
if (this.breadcrumbs.length > this.maxBreadcrumbs) {
|
|
344
|
-
this.breadcrumbs = this.breadcrumbs.slice(-this.maxBreadcrumbs);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Capture an exception and send it as an error event.
|
|
349
|
-
* Includes any accumulated breadcrumbs for debugging context.
|
|
350
|
-
*/
|
|
351
|
-
captureException(error, context) {
|
|
352
|
-
const breadcrumbSnapshot = [...this.breadcrumbs];
|
|
353
|
-
this.track({
|
|
354
|
-
event_name: '$error',
|
|
355
|
-
user_id: context?.user_id || '__system__',
|
|
356
|
-
properties: {
|
|
357
|
-
error_message: error.message,
|
|
358
|
-
error_stack: error.stack || '',
|
|
359
|
-
error_type: error.name || 'Error',
|
|
360
|
-
breadcrumbs: breadcrumbSnapshot,
|
|
361
|
-
release: this.release,
|
|
362
|
-
sdk_version: SDK_VERSION,
|
|
363
|
-
event_source: 'server_sdk',
|
|
364
|
-
...context,
|
|
365
|
-
},
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Install global error handlers for uncaught exceptions and unhandled rejections.
|
|
370
|
-
* Call once at app startup.
|
|
371
|
-
*/
|
|
372
|
-
installGlobalHandlers() {
|
|
373
|
-
process.on('uncaughtException', (error) => {
|
|
374
|
-
this.captureException(error, { source: 'uncaughtException' });
|
|
375
|
-
// Flush before exit
|
|
376
|
-
this.flush().finally(() => process.exit(1));
|
|
377
|
-
});
|
|
378
|
-
process.on('unhandledRejection', (reason) => {
|
|
379
|
-
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
380
|
-
this.captureException(error, { source: 'unhandledRejection' });
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Stop the periodic flush timer, detach process listeners, and perform a
|
|
385
|
-
* final flush. Safe to call multiple times.
|
|
386
|
-
*/
|
|
387
|
-
async shutdown() {
|
|
388
|
-
if (this.flushTimer) {
|
|
389
|
-
clearInterval(this.flushTimer);
|
|
390
|
-
this.flushTimer = null;
|
|
391
|
-
}
|
|
392
|
-
for (const detach of this.shutdownHandlers.splice(0)) {
|
|
393
|
-
try {
|
|
394
|
-
detach();
|
|
395
|
-
}
|
|
396
|
-
catch { /* best-effort */ }
|
|
397
|
-
}
|
|
398
|
-
await this.flush();
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
exports.Gurulu = Gurulu;
|
|
402
|
-
async function safeText(res) {
|
|
403
|
-
try {
|
|
404
|
-
return await res.text();
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
return '';
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
function sleep(ms) {
|
|
411
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
412
|
-
}
|
|
413
|
-
/* ------------------------------------------------------------------ */
|
|
414
|
-
/* Singleton / default instance */
|
|
415
|
-
/* ------------------------------------------------------------------ */
|
|
416
|
-
let defaultInstance = null;
|
|
417
|
-
/**
|
|
418
|
-
* Set the default Gurulu instance used by the convenience `captureException`.
|
|
419
|
-
*/
|
|
420
|
-
function setDefaultInstance(instance) {
|
|
421
|
-
defaultInstance = instance;
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Convenience function — captures an exception via the default instance.
|
|
425
|
-
* Call `setDefaultInstance()` first (or create a Gurulu client).
|
|
426
|
-
*/
|
|
427
|
-
function captureException(error, context) {
|
|
428
|
-
if (!defaultInstance) {
|
|
429
|
-
throw new Error('No default Gurulu instance. Call setDefaultInstance() first.');
|
|
430
|
-
}
|
|
431
|
-
defaultInstance.captureException(error, context);
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Convenience function — adds a breadcrumb via the default instance.
|
|
435
|
-
* Call `setDefaultInstance()` first (or create a Gurulu client).
|
|
436
|
-
*/
|
|
437
|
-
function addBreadcrumb(category, message, data) {
|
|
438
|
-
if (!defaultInstance) {
|
|
439
|
-
throw new Error('No default Gurulu instance. Call setDefaultInstance() first.');
|
|
440
|
-
}
|
|
441
|
-
defaultInstance.addBreadcrumb(category, message, data);
|
|
442
|
-
}
|
package/dist/middleware.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @gurulu/node — framework error-handler middleware.
|
|
3
|
-
*
|
|
4
|
-
* Provides drop-in error handlers for Express, Fastify, and Hono.
|
|
5
|
-
* Each handler captures the exception via `client.captureException` and
|
|
6
|
-
* then delegates to the framework's default error-handling flow.
|
|
7
|
-
*/
|
|
8
|
-
import type { GuruluClient } from './client';
|
|
9
|
-
/**
|
|
10
|
-
* Express error-handling middleware.
|
|
11
|
-
* Place AFTER all routes: app.use(guruluErrorHandler(gurulu))
|
|
12
|
-
*/
|
|
13
|
-
export declare function expressErrorHandler(client: GuruluClient): (err: Error, req: any, res: any, next: any) => void;
|
|
14
|
-
/**
|
|
15
|
-
* Express request handler wrapper.
|
|
16
|
-
* Wraps async handlers to catch thrown errors:
|
|
17
|
-
* app.get('/x', guruluHandler(gurulu, handler))
|
|
18
|
-
*/
|
|
19
|
-
export declare function expressHandler(client: GuruluClient, handler: Function): (req: any, res: any, next: any) => Promise<void>;
|
|
20
|
-
/**
|
|
21
|
-
* Fastify error handler plugin.
|
|
22
|
-
* Register: fastify.register(guruluFastifyPlugin, { client: gurulu })
|
|
23
|
-
*/
|
|
24
|
-
export declare function guruluFastifyPlugin(fastify: any, opts: {
|
|
25
|
-
client: GuruluClient;
|
|
26
|
-
}, done: Function): void;
|
|
27
|
-
/**
|
|
28
|
-
* Hono error handler middleware.
|
|
29
|
-
* Usage: app.onError(guruluHonoErrorHandler(gurulu))
|
|
30
|
-
*/
|
|
31
|
-
export declare function honoErrorHandler(client: GuruluClient): (err: Error, c: any) => any;
|
package/dist/middleware.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @gurulu/node — framework error-handler middleware.
|
|
4
|
-
*
|
|
5
|
-
* Provides drop-in error handlers for Express, Fastify, and Hono.
|
|
6
|
-
* Each handler captures the exception via `client.captureException` and
|
|
7
|
-
* then delegates to the framework's default error-handling flow.
|
|
8
|
-
*/
|
|
9
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.expressErrorHandler = expressErrorHandler;
|
|
11
|
-
exports.expressHandler = expressHandler;
|
|
12
|
-
exports.guruluFastifyPlugin = guruluFastifyPlugin;
|
|
13
|
-
exports.honoErrorHandler = honoErrorHandler;
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Express
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
/**
|
|
18
|
-
* Express error-handling middleware.
|
|
19
|
-
* Place AFTER all routes: app.use(guruluErrorHandler(gurulu))
|
|
20
|
-
*/
|
|
21
|
-
function expressErrorHandler(client) {
|
|
22
|
-
return function guruluExpressErrorHandler(err, req, res, next) {
|
|
23
|
-
client.captureException(err, {
|
|
24
|
-
mechanism: 'express_middleware',
|
|
25
|
-
request: {
|
|
26
|
-
method: req.method,
|
|
27
|
-
url: req.originalUrl || req.url,
|
|
28
|
-
headers: sanitizeHeaders(req.headers),
|
|
29
|
-
query: req.query,
|
|
30
|
-
body: truncate(req.body),
|
|
31
|
-
ip: req.ip || req.connection?.remoteAddress,
|
|
32
|
-
},
|
|
33
|
-
user: req.user ? { id: req.user.id, email: req.user.email } : undefined,
|
|
34
|
-
});
|
|
35
|
-
next(err);
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Express request handler wrapper.
|
|
40
|
-
* Wraps async handlers to catch thrown errors:
|
|
41
|
-
* app.get('/x', guruluHandler(gurulu, handler))
|
|
42
|
-
*/
|
|
43
|
-
function expressHandler(client, handler) {
|
|
44
|
-
return async function guruluWrappedHandler(req, res, next) {
|
|
45
|
-
try {
|
|
46
|
-
await handler(req, res, next);
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
client.captureException(err, {
|
|
50
|
-
mechanism: 'express_handler_wrapper',
|
|
51
|
-
request: {
|
|
52
|
-
method: req.method,
|
|
53
|
-
url: req.originalUrl || req.url,
|
|
54
|
-
headers: sanitizeHeaders(req.headers),
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
next(err);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Fastify
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
/**
|
|
65
|
-
* Fastify error handler plugin.
|
|
66
|
-
* Register: fastify.register(guruluFastifyPlugin, { client: gurulu })
|
|
67
|
-
*/
|
|
68
|
-
function guruluFastifyPlugin(fastify, opts, done) {
|
|
69
|
-
fastify.setErrorHandler(function guruluFastifyErrorHandler(error, request, reply) {
|
|
70
|
-
opts.client.captureException(error, {
|
|
71
|
-
mechanism: 'fastify_error_handler',
|
|
72
|
-
request: {
|
|
73
|
-
method: request.method,
|
|
74
|
-
url: request.url,
|
|
75
|
-
headers: sanitizeHeaders(request.headers),
|
|
76
|
-
query: request.query,
|
|
77
|
-
body: truncate(request.body),
|
|
78
|
-
ip: request.ip,
|
|
79
|
-
},
|
|
80
|
-
user: request.user
|
|
81
|
-
? { id: request.user.id, email: request.user.email }
|
|
82
|
-
: undefined,
|
|
83
|
-
});
|
|
84
|
-
// Let Fastify's default handler send the response
|
|
85
|
-
reply.status(500).send({ error: 'Internal Server Error' });
|
|
86
|
-
});
|
|
87
|
-
done();
|
|
88
|
-
}
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// Hono
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
/**
|
|
93
|
-
* Hono error handler middleware.
|
|
94
|
-
* Usage: app.onError(guruluHonoErrorHandler(gurulu))
|
|
95
|
-
*/
|
|
96
|
-
function honoErrorHandler(client) {
|
|
97
|
-
return function guruluHonoErrorHandler(err, c) {
|
|
98
|
-
client.captureException(err, {
|
|
99
|
-
mechanism: 'hono_error_handler',
|
|
100
|
-
request: {
|
|
101
|
-
method: c.req.method,
|
|
102
|
-
url: c.req.url,
|
|
103
|
-
headers: sanitizeHeaders(Object.fromEntries(c.req.raw.headers || [])),
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
return c.json({ error: 'Internal Server Error' }, 500);
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
// Helpers
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
const SENSITIVE_HEADERS = new Set([
|
|
113
|
-
'authorization',
|
|
114
|
-
'cookie',
|
|
115
|
-
'set-cookie',
|
|
116
|
-
'x-api-key',
|
|
117
|
-
'x-auth-token',
|
|
118
|
-
]);
|
|
119
|
-
function sanitizeHeaders(headers) {
|
|
120
|
-
if (!headers || typeof headers !== 'object')
|
|
121
|
-
return {};
|
|
122
|
-
const sanitized = {};
|
|
123
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
124
|
-
const lower = key.toLowerCase();
|
|
125
|
-
sanitized[lower] = SENSITIVE_HEADERS.has(lower)
|
|
126
|
-
? '[Filtered]'
|
|
127
|
-
: String(value);
|
|
128
|
-
}
|
|
129
|
-
return sanitized;
|
|
130
|
-
}
|
|
131
|
-
function truncate(body, maxLen = 2048) {
|
|
132
|
-
if (body === undefined || body === null)
|
|
133
|
-
return undefined;
|
|
134
|
-
const str = typeof body === 'string' ? body : JSON.stringify(body);
|
|
135
|
-
if (!str)
|
|
136
|
-
return undefined;
|
|
137
|
-
return str.length > maxLen ? str.slice(0, maxLen) + '...' : str;
|
|
138
|
-
}
|
package/dist/types.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Node SDK public types.
|
|
4
|
-
*
|
|
5
|
-
* Canonical event shapes (Envelope, ServerEvent, EventTier, EventSource,
|
|
6
|
-
* ConsentLevel, createEnvelope, normalizeLegacy, parseEnvelope) are re-exported
|
|
7
|
-
* from @gurulu/shared-core — single source of truth per W1.2.
|
|
8
|
-
*
|
|
9
|
-
* The only node-sdk-specific addition is the transport config
|
|
10
|
-
* (GuruluClientConfig) which wraps the shared ServerEvent with the knobs
|
|
11
|
-
* needed to actually ship events to the ingest endpoint.
|
|
12
|
-
*
|
|
13
|
-
* Related docs: PHASE-10-ROADMAP.md §W1.2, §W4.2
|
|
14
|
-
*/
|
|
15
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
-
if (k2 === undefined) k2 = k;
|
|
17
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
-
}
|
|
21
|
-
Object.defineProperty(o, k2, desc);
|
|
22
|
-
}) : (function(o, m, k, k2) {
|
|
23
|
-
if (k2 === undefined) k2 = k;
|
|
24
|
-
o[k2] = m[k];
|
|
25
|
-
}));
|
|
26
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
27
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
28
|
-
};
|
|
29
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
-
__exportStar(require("@gurulu/shared-core"), exports);
|