@gurulu/node 0.1.1 → 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.
Files changed (48) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +64 -31
  3. package/dist/context.d.ts +12 -0
  4. package/dist/context.d.ts.map +1 -0
  5. package/dist/core.d.ts +60 -0
  6. package/dist/core.d.ts.map +1 -0
  7. package/dist/errors.d.ts +32 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/identify.d.ts +9 -0
  10. package/dist/identify.d.ts.map +1 -0
  11. package/dist/index.d.ts +13 -12
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +947 -21
  14. package/dist/middleware/express.d.ts +25 -0
  15. package/dist/middleware/express.d.ts.map +1 -0
  16. package/dist/middleware/express.js +66 -0
  17. package/dist/middleware/fastify.d.ts +20 -0
  18. package/dist/middleware/fastify.d.ts.map +1 -0
  19. package/dist/middleware/fastify.js +68 -0
  20. package/dist/middleware/next.d.ts +10 -0
  21. package/dist/middleware/next.d.ts.map +1 -0
  22. package/dist/middleware/next.js +69 -0
  23. package/dist/queue.d.ts +20 -0
  24. package/dist/queue.d.ts.map +1 -0
  25. package/dist/track.d.ts +10 -0
  26. package/dist/track.d.ts.map +1 -0
  27. package/dist/transport.d.ts +16 -0
  28. package/dist/transport.d.ts.map +1 -0
  29. package/dist/types.d.ts +129 -43
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/webhooks/custom.d.ts +30 -0
  32. package/dist/webhooks/custom.d.ts.map +1 -0
  33. package/dist/webhooks/custom.js +123 -0
  34. package/dist/webhooks/lemonsqueezy.d.ts +30 -0
  35. package/dist/webhooks/lemonsqueezy.d.ts.map +1 -0
  36. package/dist/webhooks/lemonsqueezy.js +140 -0
  37. package/dist/webhooks/shopify.d.ts +18 -0
  38. package/dist/webhooks/shopify.d.ts.map +1 -0
  39. package/dist/webhooks/shopify.js +142 -0
  40. package/dist/webhooks/stripe.d.ts +31 -0
  41. package/dist/webhooks/stripe.d.ts.map +1 -0
  42. package/dist/webhooks/stripe.js +160 -0
  43. package/package.json +105 -11
  44. package/dist/business-events.d.ts +0 -73
  45. package/dist/business-events.js +0 -113
  46. package/dist/client.d.ts +0 -90
  47. package/dist/client.js +0 -307
  48. package/dist/types.js +0 -30
package/dist/client.js DELETED
@@ -1,307 +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
- const crypto_1 = require("crypto");
27
- const DEFAULT_ENDPOINT = 'https://app.gurulu.io/api/ingest/v1/server';
28
- const DEFAULT_FLUSH_INTERVAL = 5000;
29
- const DEFAULT_BATCH_SIZE = 50;
30
- const DEFAULT_TIMEOUT = 10000;
31
- const RETRY_DELAYS_MS = [200, 600, 1800];
32
- /**
33
- * Deterministic idempotency key for a server event. Hashes the tuple
34
- * (site_id, event_name, timestamp, anonymous_id/user_id) so replayed events
35
- * across retries produce the same key and the server can dedupe.
36
- */
37
- function createIdempotencyKey(event, siteId) {
38
- const parts = [
39
- siteId,
40
- event.event_name ?? '',
41
- event.timestamp ?? '',
42
- event.user_id ?? '',
43
- ];
44
- return (0, crypto_1.createHash)('sha256').update(parts.join('|')).digest('hex');
45
- }
46
- class Gurulu {
47
- siteId;
48
- apiKey;
49
- endpoint;
50
- flushInterval;
51
- batchSize;
52
- timeout;
53
- onError;
54
- onDeadLetter;
55
- fetchImpl;
56
- debug;
57
- queue = [];
58
- flushTimer = null;
59
- inFlight = null;
60
- shutdownHandlers = [];
61
- constructor(config) {
62
- if (!config.siteId)
63
- throw new Error('siteId is required');
64
- if (!config.apiKey)
65
- throw new Error('apiKey is required');
66
- this.siteId = config.siteId;
67
- this.apiKey = config.apiKey;
68
- this.endpoint = config.endpoint || DEFAULT_ENDPOINT;
69
- this.flushInterval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
70
- this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
71
- this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
72
- this.onError = config.onError;
73
- this.onDeadLetter = config.onDeadLetter;
74
- this.debug = config.debug ?? false;
75
- const injected = config.fetchImpl;
76
- if (injected) {
77
- this.fetchImpl = injected;
78
- }
79
- else {
80
- const g = globalThis;
81
- if (typeof g.fetch !== 'function') {
82
- throw new Error('global fetch is not available; pass fetchImpl in config (Node 18+ required)');
83
- }
84
- this.fetchImpl = g.fetch.bind(globalThis);
85
- }
86
- if (this.flushInterval > 0) {
87
- this.flushTimer = setInterval(() => {
88
- // Fire and forget; errors surface via onError.
89
- void this.flush();
90
- }, this.flushInterval);
91
- // Let the process exit even if the timer is pending.
92
- const t = this.flushTimer;
93
- if (typeof t.unref === 'function')
94
- t.unref();
95
- }
96
- if (!config.disableAutoShutdown && typeof process !== 'undefined' && typeof process.on === 'function') {
97
- const beforeExit = () => {
98
- void this.flush();
99
- };
100
- const onSigterm = () => {
101
- void this.shutdown();
102
- };
103
- process.on('beforeExit', beforeExit);
104
- process.on('SIGTERM', onSigterm);
105
- this.shutdownHandlers.push(() => {
106
- process.off('beforeExit', beforeExit);
107
- process.off('SIGTERM', onSigterm);
108
- });
109
- }
110
- }
111
- get queueSize() {
112
- return this.queue.length;
113
- }
114
- /**
115
- * Enqueue a server event. Auto-flushes synchronously when the queue reaches
116
- * `batchSize`. Each event is stamped with a timestamp if missing so the
117
- * idempotency key is stable across retries.
118
- */
119
- track(event) {
120
- if (!event || !event.event_name) {
121
- if (this.debug)
122
- console.warn('[gurulu] event_name is required');
123
- return;
124
- }
125
- if (!event.user_id) {
126
- if (this.debug)
127
- console.warn('[gurulu] user_id is required for server events');
128
- return;
129
- }
130
- const stamped = {
131
- ...event,
132
- timestamp: event.timestamp ?? new Date().toISOString(),
133
- };
134
- this.queue.push(stamped);
135
- if (this.debug) {
136
- console.log(`[gurulu] queued: ${stamped.event_name} (${this.queue.length}/${this.batchSize})`);
137
- }
138
- if (this.queue.length >= this.batchSize) {
139
- void this.flush();
140
- }
141
- }
142
- /**
143
- * POST all queued events in one batch. On 5xx / network error retries with
144
- * exponential backoff (200ms, 600ms, 1800ms). On 4xx drops the batch and
145
- * fires the dead-letter callback. Concurrent flush calls are serialized.
146
- */
147
- async flush() {
148
- // Serialize concurrent flushes so retries don't interleave.
149
- if (this.inFlight) {
150
- await this.inFlight;
151
- }
152
- if (this.queue.length === 0)
153
- return;
154
- const batch = this.queue.splice(0, this.queue.length);
155
- const run = this.sendBatch(batch);
156
- this.inFlight = run.finally(() => {
157
- this.inFlight = null;
158
- });
159
- await this.inFlight;
160
- }
161
- async sendBatch(batch) {
162
- // Idempotency key for the whole batch — hashed over individual event keys.
163
- const perEventKeys = batch.map((e) => createIdempotencyKey(e, this.siteId));
164
- const batchKey = (0, crypto_1.createHash)('sha256').update(perEventKeys.join('|')).digest('hex');
165
- const body = JSON.stringify({
166
- site_id: this.siteId,
167
- events: batch,
168
- });
169
- const headers = {
170
- 'Content-Type': 'application/json',
171
- Authorization: `Bearer ${this.apiKey}`,
172
- 'Idempotency-Key': batchKey,
173
- };
174
- let lastError = null;
175
- for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt++) {
176
- try {
177
- const res = await this.fetchImpl(this.endpoint, {
178
- method: 'POST',
179
- headers,
180
- body,
181
- });
182
- if (res.ok) {
183
- if (this.debug)
184
- console.log(`[gurulu] flushed ${batch.length} events`);
185
- return;
186
- }
187
- if (res.status >= 400 && res.status < 500) {
188
- // Client error — don't retry. Drop + dead-letter.
189
- const text = await safeText(res);
190
- const err = new Error(`client_error ${res.status}: ${text}`);
191
- if (this.debug)
192
- console.error(`[gurulu] ${err.message}`);
193
- this.onError?.(err);
194
- this.onDeadLetter?.(batch, {
195
- kind: 'client_error',
196
- status: res.status,
197
- message: text,
198
- });
199
- return;
200
- }
201
- lastError = new Error(`server_error ${res.status}`);
202
- this.onError?.(lastError);
203
- }
204
- catch (err) {
205
- lastError = err instanceof Error ? err : new Error(String(err));
206
- this.onError?.(lastError);
207
- }
208
- // Not the last attempt — sleep with the fixed exponential schedule.
209
- if (attempt < RETRY_DELAYS_MS.length - 1) {
210
- await sleep(RETRY_DELAYS_MS[attempt + 1 - 1]);
211
- // ^ Index arithmetic: attempt=0 -> use RETRY_DELAYS_MS[0]=200, etc.
212
- }
213
- }
214
- // All 3 attempts failed.
215
- if (this.debug) {
216
- console.error(`[gurulu] dropping batch of ${batch.length} after retries: ${lastError?.message}`);
217
- }
218
- if (this.onDeadLetter) {
219
- this.onDeadLetter(batch, {
220
- kind: lastError?.message.startsWith('server_error') ? 'server_error' : 'network_error',
221
- message: lastError?.message ?? 'unknown error',
222
- });
223
- }
224
- else if (this.debug) {
225
- console.error('[gurulu] no dead-letter callback configured; events lost');
226
- }
227
- }
228
- /**
229
- * Capture an exception and send it as an error event.
230
- */
231
- captureException(error, context) {
232
- this.track({
233
- event_name: '$error',
234
- user_id: context?.user_id || '__system__',
235
- properties: {
236
- error_message: error.message,
237
- error_stack: error.stack || '',
238
- error_type: error.name || 'Error',
239
- ...context,
240
- },
241
- });
242
- }
243
- /**
244
- * Install global error handlers for uncaught exceptions and unhandled rejections.
245
- * Call once at app startup.
246
- */
247
- installGlobalHandlers() {
248
- process.on('uncaughtException', (error) => {
249
- this.captureException(error, { source: 'uncaughtException' });
250
- // Flush before exit
251
- this.flush().finally(() => process.exit(1));
252
- });
253
- process.on('unhandledRejection', (reason) => {
254
- const error = reason instanceof Error ? reason : new Error(String(reason));
255
- this.captureException(error, { source: 'unhandledRejection' });
256
- });
257
- }
258
- /**
259
- * Stop the periodic flush timer, detach process listeners, and perform a
260
- * final flush. Safe to call multiple times.
261
- */
262
- async shutdown() {
263
- if (this.flushTimer) {
264
- clearInterval(this.flushTimer);
265
- this.flushTimer = null;
266
- }
267
- for (const detach of this.shutdownHandlers.splice(0)) {
268
- try {
269
- detach();
270
- }
271
- catch { /* best-effort */ }
272
- }
273
- await this.flush();
274
- }
275
- }
276
- exports.Gurulu = Gurulu;
277
- async function safeText(res) {
278
- try {
279
- return await res.text();
280
- }
281
- catch {
282
- return '';
283
- }
284
- }
285
- function sleep(ms) {
286
- return new Promise((resolve) => setTimeout(resolve, ms));
287
- }
288
- /* ------------------------------------------------------------------ */
289
- /* Singleton / default instance */
290
- /* ------------------------------------------------------------------ */
291
- let defaultInstance = null;
292
- /**
293
- * Set the default Gurulu instance used by the convenience `captureException`.
294
- */
295
- function setDefaultInstance(instance) {
296
- defaultInstance = instance;
297
- }
298
- /**
299
- * Convenience function — captures an exception via the default instance.
300
- * Call `setDefaultInstance()` first (or create a Gurulu client).
301
- */
302
- function captureException(error, context) {
303
- if (!defaultInstance) {
304
- throw new Error('No default Gurulu instance. Call setDefaultInstance() first.');
305
- }
306
- defaultInstance.captureException(error, context);
307
- }
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);