@gurulu/node 0.1.1 → 0.1.2
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/dist/business-events.d.ts +2 -2
- package/dist/business-events.js +2 -4
- package/dist/client.d.ts +60 -0
- package/dist/client.js +137 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +8 -1
- package/dist/middleware.d.ts +31 -0
- package/dist/middleware.js +138 -0
- package/dist/types.d.ts +2 -0
- package/package.json +14 -1
|
@@ -49,8 +49,8 @@ export declare function paymentSucceeded(args: PaymentSucceededArgs): BusinessEv
|
|
|
49
49
|
export interface SubscriptionStartedArgs {
|
|
50
50
|
userId: string;
|
|
51
51
|
plan: string;
|
|
52
|
-
amount
|
|
53
|
-
currency
|
|
52
|
+
amount: number;
|
|
53
|
+
currency: string;
|
|
54
54
|
}
|
|
55
55
|
export declare function subscriptionStarted(args: SubscriptionStartedArgs): BusinessEvent;
|
|
56
56
|
export interface OrderPlacedArgs {
|
package/dist/business-events.js
CHANGED
|
@@ -80,11 +80,9 @@ function paymentSucceeded(args) {
|
|
|
80
80
|
function subscriptionStarted(args) {
|
|
81
81
|
const props = {
|
|
82
82
|
plan: args.plan,
|
|
83
|
+
amount: args.amount,
|
|
84
|
+
currency: args.currency,
|
|
83
85
|
};
|
|
84
|
-
if (args.amount !== undefined)
|
|
85
|
-
props.amount = args.amount;
|
|
86
|
-
if (args.currency !== undefined)
|
|
87
|
-
props.currency = args.currency;
|
|
88
86
|
const env = baseEnvelope('subscription_started', args.userId, props);
|
|
89
87
|
return toBusinessEvent(env, args.userId);
|
|
90
88
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -19,11 +19,46 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { ServerEvent } from '@gurulu/shared-core';
|
|
21
21
|
import type { GuruluClientConfig } from './types';
|
|
22
|
+
export interface IdentifyParams {
|
|
23
|
+
/** The known user ID (e.g. database ID, email, etc.) */
|
|
24
|
+
userId: string;
|
|
25
|
+
/** An anonymous/session ID to link with this user. Required by the identify endpoint. */
|
|
26
|
+
anonymousId: string;
|
|
27
|
+
/** User traits (email, name, plan, etc.) to persist on the identity profile. */
|
|
28
|
+
traits?: Record<string, unknown>;
|
|
29
|
+
/** Device ID for cross-device identity resolution. */
|
|
30
|
+
deviceId?: string;
|
|
31
|
+
/** OAuth provider name (e.g. 'google', 'github'). */
|
|
32
|
+
oauthProvider?: string;
|
|
33
|
+
/** OAuth user ID from the provider. */
|
|
34
|
+
oauthId?: string;
|
|
35
|
+
/** Consent level: 'none' | 'analytics' | 'marketing' | 'full'. */
|
|
36
|
+
consentLevel?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface IdentifyResponse {
|
|
39
|
+
status: string;
|
|
40
|
+
canonical_id: string | null;
|
|
41
|
+
consent_skipped?: boolean;
|
|
42
|
+
merges?: Array<{
|
|
43
|
+
winner: string;
|
|
44
|
+
loser: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
claim_types: string[];
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
export interface Breadcrumb {
|
|
50
|
+
timestamp: string;
|
|
51
|
+
category: string;
|
|
52
|
+
message: string;
|
|
53
|
+
data?: Record<string, unknown>;
|
|
54
|
+
}
|
|
22
55
|
export interface GuruluClient {
|
|
23
56
|
track(event: ServerEvent): void;
|
|
57
|
+
identify(params: IdentifyParams): Promise<IdentifyResponse>;
|
|
24
58
|
flush(): Promise<void>;
|
|
25
59
|
shutdown(): Promise<void>;
|
|
26
60
|
captureException(error: Error, context?: Record<string, unknown>): void;
|
|
61
|
+
addBreadcrumb(category: string, message: string, data?: Record<string, unknown>): void;
|
|
27
62
|
installGlobalHandlers(): void;
|
|
28
63
|
/** Current queue length — exposed for tests and observability. */
|
|
29
64
|
readonly queueSize: number;
|
|
@@ -45,18 +80,32 @@ export declare class Gurulu implements GuruluClient {
|
|
|
45
80
|
private readonly onDeadLetter?;
|
|
46
81
|
private readonly fetchImpl;
|
|
47
82
|
private readonly debug;
|
|
83
|
+
private readonly release;
|
|
48
84
|
private queue;
|
|
85
|
+
private breadcrumbs;
|
|
86
|
+
private readonly maxBreadcrumbs;
|
|
49
87
|
private flushTimer;
|
|
50
88
|
private inFlight;
|
|
51
89
|
private shutdownHandlers;
|
|
52
90
|
constructor(config: GuruluClientConfig);
|
|
53
91
|
get queueSize(): number;
|
|
92
|
+
private static readonly REVENUE_EVENTS;
|
|
54
93
|
/**
|
|
55
94
|
* Enqueue a server event. Auto-flushes synchronously when the queue reaches
|
|
56
95
|
* `batchSize`. Each event is stamped with a timestamp if missing so the
|
|
57
96
|
* idempotency key is stable across retries.
|
|
58
97
|
*/
|
|
59
98
|
track(event: ServerEvent): void;
|
|
99
|
+
/**
|
|
100
|
+
* Identify a user — links an anonymous session to a known user ID and
|
|
101
|
+
* persists traits on the identity profile. Sends immediately to the
|
|
102
|
+
* dedicated `/api/ingest/v1/identify` endpoint (not batched with track
|
|
103
|
+
* events) because identity resolution is latency-sensitive and returns
|
|
104
|
+
* the canonical_id synchronously.
|
|
105
|
+
*
|
|
106
|
+
* Retries with the same exponential-backoff strategy as batch flushes.
|
|
107
|
+
*/
|
|
108
|
+
identify(params: IdentifyParams): Promise<IdentifyResponse>;
|
|
60
109
|
/**
|
|
61
110
|
* POST all queued events in one batch. On 5xx / network error retries with
|
|
62
111
|
* exponential backoff (200ms, 600ms, 1800ms). On 4xx drops the batch and
|
|
@@ -64,8 +113,14 @@ export declare class Gurulu implements GuruluClient {
|
|
|
64
113
|
*/
|
|
65
114
|
flush(): Promise<void>;
|
|
66
115
|
private sendBatch;
|
|
116
|
+
/**
|
|
117
|
+
* Add a custom breadcrumb to the trail. Breadcrumbs are sent with error
|
|
118
|
+
* events to provide context about what happened before the error occurred.
|
|
119
|
+
*/
|
|
120
|
+
addBreadcrumb(category: string, message: string, data?: Record<string, unknown>): void;
|
|
67
121
|
/**
|
|
68
122
|
* Capture an exception and send it as an error event.
|
|
123
|
+
* Includes any accumulated breadcrumbs for debugging context.
|
|
69
124
|
*/
|
|
70
125
|
captureException(error: Error, context?: Record<string, unknown>): void;
|
|
71
126
|
/**
|
|
@@ -88,3 +143,8 @@ export declare function setDefaultInstance(instance: Gurulu): void;
|
|
|
88
143
|
* Call `setDefaultInstance()` first (or create a Gurulu client).
|
|
89
144
|
*/
|
|
90
145
|
export declare function captureException(error: Error, context?: Record<string, unknown>): void;
|
|
146
|
+
/**
|
|
147
|
+
* Convenience function — adds a breadcrumb via the default instance.
|
|
148
|
+
* Call `setDefaultInstance()` first (or create a Gurulu client).
|
|
149
|
+
*/
|
|
150
|
+
export declare function addBreadcrumb(category: string, message: string, data?: Record<string, unknown>): void;
|
package/dist/client.js
CHANGED
|
@@ -23,8 +23,10 @@ exports.Gurulu = void 0;
|
|
|
23
23
|
exports.createIdempotencyKey = createIdempotencyKey;
|
|
24
24
|
exports.setDefaultInstance = setDefaultInstance;
|
|
25
25
|
exports.captureException = captureException;
|
|
26
|
+
exports.addBreadcrumb = addBreadcrumb;
|
|
26
27
|
const crypto_1 = require("crypto");
|
|
27
28
|
const DEFAULT_ENDPOINT = 'https://app.gurulu.io/api/ingest/v1/server';
|
|
29
|
+
const SDK_VERSION = 'node@0.1.0';
|
|
28
30
|
const DEFAULT_FLUSH_INTERVAL = 5000;
|
|
29
31
|
const DEFAULT_BATCH_SIZE = 50;
|
|
30
32
|
const DEFAULT_TIMEOUT = 10000;
|
|
@@ -54,7 +56,10 @@ class Gurulu {
|
|
|
54
56
|
onDeadLetter;
|
|
55
57
|
fetchImpl;
|
|
56
58
|
debug;
|
|
59
|
+
release;
|
|
57
60
|
queue = [];
|
|
61
|
+
breadcrumbs = [];
|
|
62
|
+
maxBreadcrumbs = 50;
|
|
58
63
|
flushTimer = null;
|
|
59
64
|
inFlight = null;
|
|
60
65
|
shutdownHandlers = [];
|
|
@@ -72,6 +77,7 @@ class Gurulu {
|
|
|
72
77
|
this.onError = config.onError;
|
|
73
78
|
this.onDeadLetter = config.onDeadLetter;
|
|
74
79
|
this.debug = config.debug ?? false;
|
|
80
|
+
this.release = config.release || process.env.GURULU_RELEASE || '';
|
|
75
81
|
const injected = config.fetchImpl;
|
|
76
82
|
if (injected) {
|
|
77
83
|
this.fetchImpl = injected;
|
|
@@ -111,6 +117,14 @@ class Gurulu {
|
|
|
111
117
|
get queueSize() {
|
|
112
118
|
return this.queue.length;
|
|
113
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
|
+
]);
|
|
114
128
|
/**
|
|
115
129
|
* Enqueue a server event. Auto-flushes synchronously when the queue reaches
|
|
116
130
|
* `batchSize`. Each event is stamped with a timestamp if missing so the
|
|
@@ -127,6 +141,19 @@ class Gurulu {
|
|
|
127
141
|
console.warn('[gurulu] user_id is required for server events');
|
|
128
142
|
return;
|
|
129
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
|
+
}
|
|
130
157
|
const stamped = {
|
|
131
158
|
...event,
|
|
132
159
|
timestamp: event.timestamp ?? new Date().toISOString(),
|
|
@@ -139,6 +166,71 @@ class Gurulu {
|
|
|
139
166
|
void this.flush();
|
|
140
167
|
}
|
|
141
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
|
+
}
|
|
142
234
|
/**
|
|
143
235
|
* POST all queued events in one batch. On 5xx / network error retries with
|
|
144
236
|
* exponential backoff (200ms, 600ms, 1800ms). On 4xx drops the batch and
|
|
@@ -173,12 +265,16 @@ class Gurulu {
|
|
|
173
265
|
};
|
|
174
266
|
let lastError = null;
|
|
175
267
|
for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt++) {
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
176
270
|
try {
|
|
177
271
|
const res = await this.fetchImpl(this.endpoint, {
|
|
178
272
|
method: 'POST',
|
|
179
273
|
headers,
|
|
180
274
|
body,
|
|
275
|
+
signal: controller.signal,
|
|
181
276
|
});
|
|
277
|
+
clearTimeout(timeoutId);
|
|
182
278
|
if (res.ok) {
|
|
183
279
|
if (this.debug)
|
|
184
280
|
console.log(`[gurulu] flushed ${batch.length} events`);
|
|
@@ -202,8 +298,16 @@ class Gurulu {
|
|
|
202
298
|
this.onError?.(lastError);
|
|
203
299
|
}
|
|
204
300
|
catch (err) {
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}
|
|
207
311
|
}
|
|
208
312
|
// Not the last attempt — sleep with the fixed exponential schedule.
|
|
209
313
|
if (attempt < RETRY_DELAYS_MS.length - 1) {
|
|
@@ -225,10 +329,27 @@ class Gurulu {
|
|
|
225
329
|
console.error('[gurulu] no dead-letter callback configured; events lost');
|
|
226
330
|
}
|
|
227
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
|
+
}
|
|
228
347
|
/**
|
|
229
348
|
* Capture an exception and send it as an error event.
|
|
349
|
+
* Includes any accumulated breadcrumbs for debugging context.
|
|
230
350
|
*/
|
|
231
351
|
captureException(error, context) {
|
|
352
|
+
const breadcrumbSnapshot = [...this.breadcrumbs];
|
|
232
353
|
this.track({
|
|
233
354
|
event_name: '$error',
|
|
234
355
|
user_id: context?.user_id || '__system__',
|
|
@@ -236,6 +357,10 @@ class Gurulu {
|
|
|
236
357
|
error_message: error.message,
|
|
237
358
|
error_stack: error.stack || '',
|
|
238
359
|
error_type: error.name || 'Error',
|
|
360
|
+
breadcrumbs: breadcrumbSnapshot,
|
|
361
|
+
release: this.release,
|
|
362
|
+
sdk_version: SDK_VERSION,
|
|
363
|
+
event_source: 'server_sdk',
|
|
239
364
|
...context,
|
|
240
365
|
},
|
|
241
366
|
});
|
|
@@ -305,3 +430,13 @@ function captureException(error, context) {
|
|
|
305
430
|
}
|
|
306
431
|
defaultInstance.captureException(error, context);
|
|
307
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/index.d.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Phase 10 W4.2: server SDK hardening adds batch+retry, deterministic
|
|
5
5
|
* idempotency keys, dead-letter callbacks, and business event emitters.
|
|
6
6
|
*/
|
|
7
|
-
export { Gurulu, createIdempotencyKey, captureException, setDefaultInstance } from './client';
|
|
8
|
-
export type { GuruluClient } from './client';
|
|
7
|
+
export { Gurulu, createIdempotencyKey, captureException, addBreadcrumb, setDefaultInstance } from './client';
|
|
8
|
+
export type { GuruluClient, IdentifyParams, IdentifyResponse, Breadcrumb } from './client';
|
|
9
9
|
export type { GuruluClientConfig, DeadLetterCallback, ErrorCallback, } from './types';
|
|
10
10
|
export type { Envelope, ServerEvent, EventTier, EventSource, ConsentLevel, } from '@gurulu/shared-core';
|
|
11
|
+
export { expressErrorHandler, expressHandler, guruluFastifyPlugin, honoErrorHandler, } from './middleware';
|
|
11
12
|
export { userCreated, paymentSucceeded, subscriptionStarted, orderPlaced, leadCaptured, } from './business-events';
|
|
12
13
|
export type { BusinessEvent, UserCreatedArgs, PaymentSucceededArgs, SubscriptionStartedArgs, OrderPlacedArgs, LeadCapturedArgs, } from './business-events';
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
* idempotency keys, dead-letter callbacks, and business event emitters.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.leadCaptured = exports.orderPlaced = exports.subscriptionStarted = exports.paymentSucceeded = exports.userCreated = exports.setDefaultInstance = exports.captureException = exports.createIdempotencyKey = exports.Gurulu = void 0;
|
|
9
|
+
exports.leadCaptured = exports.orderPlaced = exports.subscriptionStarted = exports.paymentSucceeded = exports.userCreated = exports.honoErrorHandler = exports.guruluFastifyPlugin = exports.expressHandler = exports.expressErrorHandler = exports.setDefaultInstance = exports.addBreadcrumb = exports.captureException = exports.createIdempotencyKey = exports.Gurulu = void 0;
|
|
10
10
|
var client_1 = require("./client");
|
|
11
11
|
Object.defineProperty(exports, "Gurulu", { enumerable: true, get: function () { return client_1.Gurulu; } });
|
|
12
12
|
Object.defineProperty(exports, "createIdempotencyKey", { enumerable: true, get: function () { return client_1.createIdempotencyKey; } });
|
|
13
13
|
Object.defineProperty(exports, "captureException", { enumerable: true, get: function () { return client_1.captureException; } });
|
|
14
|
+
Object.defineProperty(exports, "addBreadcrumb", { enumerable: true, get: function () { return client_1.addBreadcrumb; } });
|
|
14
15
|
Object.defineProperty(exports, "setDefaultInstance", { enumerable: true, get: function () { return client_1.setDefaultInstance; } });
|
|
16
|
+
// Framework error-handler middleware.
|
|
17
|
+
var middleware_1 = require("./middleware");
|
|
18
|
+
Object.defineProperty(exports, "expressErrorHandler", { enumerable: true, get: function () { return middleware_1.expressErrorHandler; } });
|
|
19
|
+
Object.defineProperty(exports, "expressHandler", { enumerable: true, get: function () { return middleware_1.expressHandler; } });
|
|
20
|
+
Object.defineProperty(exports, "guruluFastifyPlugin", { enumerable: true, get: function () { return middleware_1.guruluFastifyPlugin; } });
|
|
21
|
+
Object.defineProperty(exports, "honoErrorHandler", { enumerable: true, get: function () { return middleware_1.honoErrorHandler; } });
|
|
15
22
|
// Business event emitters (W4.2).
|
|
16
23
|
var business_events_1 = require("./business-events");
|
|
17
24
|
Object.defineProperty(exports, "userCreated", { enumerable: true, get: function () { return business_events_1.userCreated; } });
|
|
@@ -0,0 +1,31 @@
|
|
|
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;
|
|
@@ -0,0 +1,138 @@
|
|
|
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.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type ErrorCallback = (err: Error) => void;
|
|
|
27
27
|
export interface GuruluClientConfig {
|
|
28
28
|
siteId: string;
|
|
29
29
|
apiKey: string;
|
|
30
|
+
/** Release version or git SHA for source map resolution. Falls back to GURULU_RELEASE env var. */
|
|
31
|
+
release?: string;
|
|
30
32
|
endpoint?: string;
|
|
31
33
|
/** Periodic flush interval in ms. Default 5000. */
|
|
32
34
|
flushInterval?: number;
|
package/package.json
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gurulu/node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Gurulu.io server-side analytics SDK for Node.js",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": ["dist"],
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./middleware": {
|
|
15
|
+
"types": "./dist/middleware.d.ts",
|
|
16
|
+
"import": "./dist/middleware.js",
|
|
17
|
+
"require": "./dist/middleware.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
8
21
|
"scripts": {
|
|
9
22
|
"build": "tsc",
|
|
10
23
|
"dev": "tsc --watch"
|