@gurulu/node 0.1.0 → 0.1.1

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.
@@ -23,6 +23,8 @@ export interface GuruluClient {
23
23
  track(event: ServerEvent): void;
24
24
  flush(): Promise<void>;
25
25
  shutdown(): Promise<void>;
26
+ captureException(error: Error, context?: Record<string, unknown>): void;
27
+ installGlobalHandlers(): void;
26
28
  /** Current queue length — exposed for tests and observability. */
27
29
  readonly queueSize: number;
28
30
  }
@@ -62,9 +64,27 @@ export declare class Gurulu implements GuruluClient {
62
64
  */
63
65
  flush(): Promise<void>;
64
66
  private sendBatch;
67
+ /**
68
+ * Capture an exception and send it as an error event.
69
+ */
70
+ captureException(error: Error, context?: Record<string, unknown>): void;
71
+ /**
72
+ * Install global error handlers for uncaught exceptions and unhandled rejections.
73
+ * Call once at app startup.
74
+ */
75
+ installGlobalHandlers(): void;
65
76
  /**
66
77
  * Stop the periodic flush timer, detach process listeners, and perform a
67
78
  * final flush. Safe to call multiple times.
68
79
  */
69
80
  shutdown(): Promise<void>;
70
81
  }
82
+ /**
83
+ * Set the default Gurulu instance used by the convenience `captureException`.
84
+ */
85
+ export declare function setDefaultInstance(instance: Gurulu): void;
86
+ /**
87
+ * Convenience function — captures an exception via the default instance.
88
+ * Call `setDefaultInstance()` first (or create a Gurulu client).
89
+ */
90
+ export declare function captureException(error: Error, context?: Record<string, unknown>): void;
@@ -21,6 +21,8 @@
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.Gurulu = void 0;
23
23
  exports.createIdempotencyKey = createIdempotencyKey;
24
+ exports.setDefaultInstance = setDefaultInstance;
25
+ exports.captureException = captureException;
24
26
  const crypto_1 = require("crypto");
25
27
  const DEFAULT_ENDPOINT = 'https://app.gurulu.io/api/ingest/v1/server';
26
28
  const DEFAULT_FLUSH_INTERVAL = 5000;
@@ -223,6 +225,36 @@ class Gurulu {
223
225
  console.error('[gurulu] no dead-letter callback configured; events lost');
224
226
  }
225
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
+ }
226
258
  /**
227
259
  * Stop the periodic flush timer, detach process listeners, and perform a
228
260
  * final flush. Safe to call multiple times.
@@ -253,3 +285,23 @@ async function safeText(res) {
253
285
  function sleep(ms) {
254
286
  return new Promise((resolve) => setTimeout(resolve, ms));
255
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
+ }
@@ -4,7 +4,7 @@
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 } from './client';
7
+ export { Gurulu, createIdempotencyKey, captureException, setDefaultInstance } from './client';
8
8
  export type { GuruluClient } from './client';
9
9
  export type { GuruluClientConfig, DeadLetterCallback, ErrorCallback, } from './types';
10
10
  export type { Envelope, ServerEvent, EventTier, EventSource, ConsentLevel, } from '@gurulu/shared-core';
@@ -6,10 +6,12 @@
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.createIdempotencyKey = exports.Gurulu = void 0;
9
+ exports.leadCaptured = exports.orderPlaced = exports.subscriptionStarted = exports.paymentSucceeded = exports.userCreated = exports.setDefaultInstance = 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
+ Object.defineProperty(exports, "captureException", { enumerable: true, get: function () { return client_1.captureException; } });
14
+ Object.defineProperty(exports, "setDefaultInstance", { enumerable: true, get: function () { return client_1.setDefaultInstance; } });
13
15
  // Business event emitters (W4.2).
14
16
  var business_events_1 = require("./business-events");
15
17
  Object.defineProperty(exports, "userCreated", { enumerable: true, get: function () { return business_events_1.userCreated; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gurulu/node",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
@@ -1,35 +0,0 @@
1
- /**
2
- * Canonical Event Catalog — standardised event names and typed property
3
- * schemas shared across all Gurulu SDKs (web, iOS, Android, server).
4
- *
5
- * Every SDK exposes typed convenience helpers that delegate to the generic
6
- * `track()` method using the `$`-prefixed event names defined here. Users
7
- * can still call `track('$purchase', { ... })` directly; the helpers are
8
- * purely ergonomic.
9
- *
10
- * Property names are **snake_case** on all platforms.
11
- */
12
- export interface EventPropertySchema {
13
- name: string;
14
- type: 'string' | 'number' | 'boolean' | 'array' | 'object';
15
- required: boolean;
16
- description: string;
17
- }
18
- export type EventCategory = 'core' | 'acquisition' | 'activation' | 'revenue' | 'engagement' | 'retention' | 'referral';
19
- export interface CanonicalEvent {
20
- /** Dollar-prefixed canonical name, e.g. `$purchase`. */
21
- name: string;
22
- /** Human-readable label for dashboards. */
23
- displayName: string;
24
- /** AARRR pirate-metrics category. */
25
- category: EventCategory;
26
- description: string;
27
- properties: EventPropertySchema[];
28
- }
29
- export declare const CANONICAL_EVENTS: CanonicalEvent[];
30
- /** Retrieve a canonical event definition by its `$`-prefixed name. */
31
- export declare function getCanonicalEvent(name: string): CanonicalEvent | undefined;
32
- /** All canonical event names as a typed union. */
33
- export type CanonicalEventName = (typeof CANONICAL_EVENTS)[number]['name'];
34
- /** Set of all canonical event names for quick membership checks. */
35
- export declare const CANONICAL_EVENT_NAMES: ReadonlySet<string>;
@@ -1,225 +0,0 @@
1
- "use strict";
2
- /**
3
- * Canonical Event Catalog — standardised event names and typed property
4
- * schemas shared across all Gurulu SDKs (web, iOS, Android, server).
5
- *
6
- * Every SDK exposes typed convenience helpers that delegate to the generic
7
- * `track()` method using the `$`-prefixed event names defined here. Users
8
- * can still call `track('$purchase', { ... })` directly; the helpers are
9
- * purely ergonomic.
10
- *
11
- * Property names are **snake_case** on all platforms.
12
- */
13
- Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.CANONICAL_EVENT_NAMES = exports.CANONICAL_EVENTS = void 0;
15
- exports.getCanonicalEvent = getCanonicalEvent;
16
- // ── Helper to reduce boilerplate ────────────────────────────────────────────
17
- function prop(name, type, description, required = false) {
18
- return { name, type, required, description };
19
- }
20
- // ── Catalog ─────────────────────────────────────────────────────────────────
21
- exports.CANONICAL_EVENTS = [
22
- // ─── Core ───────────────────────────────────────────────────────────────
23
- {
24
- name: '$page_view',
25
- displayName: 'Page View',
26
- category: 'core',
27
- description: 'Fired when a user views a page or screen.',
28
- properties: [
29
- prop('url', 'string', 'Full URL of the page', true),
30
- prop('title', 'string', 'Document title'),
31
- prop('referrer', 'string', 'Referring URL'),
32
- ],
33
- },
34
- {
35
- name: '$session_start',
36
- displayName: 'Session Start',
37
- category: 'core',
38
- description: 'Marks the beginning of a new session.',
39
- properties: [],
40
- },
41
- {
42
- name: '$session_end',
43
- displayName: 'Session End',
44
- category: 'core',
45
- description: 'Marks the end of a session.',
46
- properties: [
47
- prop('duration_sec', 'number', 'Total session duration in seconds'),
48
- ],
49
- },
50
- // ─── Acquisition ────────────────────────────────────────────────────────
51
- {
52
- name: '$signup',
53
- displayName: 'Sign Up',
54
- category: 'acquisition',
55
- description: 'User created a new account.',
56
- properties: [
57
- prop('method', 'string', 'Authentication method (email, google, apple, github)'),
58
- ],
59
- },
60
- {
61
- name: '$app_install',
62
- displayName: 'App Install',
63
- category: 'acquisition',
64
- description: 'Native app was installed on the device.',
65
- properties: [
66
- prop('install_source', 'string', 'Attribution source (e.g. play_store, app_store, ad_network)'),
67
- prop('referrer_url', 'string', 'Install referrer URL'),
68
- ],
69
- },
70
- {
71
- name: '$first_open',
72
- displayName: 'First Open',
73
- category: 'acquisition',
74
- description: 'First time the app is opened after install.',
75
- properties: [],
76
- },
77
- // ─── Activation ─────────────────────────────────────────────────────────
78
- {
79
- name: '$login',
80
- displayName: 'Login',
81
- category: 'activation',
82
- description: 'User authenticated into an existing account.',
83
- properties: [
84
- prop('method', 'string', 'Authentication method (email, google, apple, github)'),
85
- ],
86
- },
87
- {
88
- name: '$onboarding_complete',
89
- displayName: 'Onboarding Complete',
90
- category: 'activation',
91
- description: 'User finished the onboarding flow.',
92
- properties: [],
93
- },
94
- // ─── Revenue ────────────────────────────────────────────────────────────
95
- {
96
- name: '$purchase',
97
- displayName: 'Purchase',
98
- category: 'revenue',
99
- description: 'A completed purchase or transaction.',
100
- properties: [
101
- prop('value', 'number', 'Monetary value of the transaction', true),
102
- prop('currency', 'string', 'ISO 4217 currency code', true),
103
- prop('transaction_id', 'string', 'Unique transaction identifier'),
104
- prop('items', 'array', 'Array of purchased item objects'),
105
- prop('payment_method', 'string', 'Payment method (credit_card, paypal, etc.)'),
106
- ],
107
- },
108
- {
109
- name: '$add_to_cart',
110
- displayName: 'Add to Cart',
111
- category: 'revenue',
112
- description: 'Item added to shopping cart.',
113
- properties: [
114
- prop('value', 'number', 'Monetary value of the item'),
115
- prop('currency', 'string', 'ISO 4217 currency code'),
116
- prop('item_id', 'string', 'Unique item identifier'),
117
- prop('item_name', 'string', 'Human-readable item name'),
118
- ],
119
- },
120
- {
121
- name: '$remove_from_cart',
122
- displayName: 'Remove from Cart',
123
- category: 'revenue',
124
- description: 'Item removed from shopping cart.',
125
- properties: [
126
- prop('value', 'number', 'Monetary value of the item'),
127
- prop('item_id', 'string', 'Unique item identifier'),
128
- ],
129
- },
130
- {
131
- name: '$begin_checkout',
132
- displayName: 'Begin Checkout',
133
- category: 'revenue',
134
- description: 'User started the checkout flow.',
135
- properties: [
136
- prop('value', 'number', 'Total cart value'),
137
- prop('currency', 'string', 'ISO 4217 currency code'),
138
- prop('item_count', 'number', 'Number of items in the cart'),
139
- ],
140
- },
141
- {
142
- name: '$subscribe',
143
- displayName: 'Subscribe',
144
- category: 'revenue',
145
- description: 'User started a recurring subscription.',
146
- properties: [
147
- prop('value', 'number', 'Subscription price', true),
148
- prop('currency', 'string', 'ISO 4217 currency code', true),
149
- prop('plan_id', 'string', 'Plan or tier identifier'),
150
- prop('interval', 'string', 'Billing interval (monthly, yearly)'),
151
- ],
152
- },
153
- {
154
- name: '$refund',
155
- displayName: 'Refund',
156
- category: 'revenue',
157
- description: 'A refund was issued.',
158
- properties: [
159
- prop('value', 'number', 'Refund amount', true),
160
- prop('currency', 'string', 'ISO 4217 currency code'),
161
- prop('transaction_id', 'string', 'Original transaction identifier'),
162
- ],
163
- },
164
- // ─── Engagement ─────────────────────────────────────────────────────────
165
- {
166
- name: '$search',
167
- displayName: 'Search',
168
- category: 'engagement',
169
- description: 'User performed a search.',
170
- properties: [
171
- prop('query', 'string', 'Search query string', true),
172
- prop('results_count', 'number', 'Number of results returned'),
173
- ],
174
- },
175
- {
176
- name: '$share',
177
- displayName: 'Share',
178
- category: 'engagement',
179
- description: 'User shared content.',
180
- properties: [
181
- prop('method', 'string', 'Share method (copy_link, twitter, email, etc.)'),
182
- prop('content_type', 'string', 'Type of content shared'),
183
- prop('item_id', 'string', 'Identifier of the shared item'),
184
- ],
185
- },
186
- {
187
- name: '$rate',
188
- displayName: 'Rate',
189
- category: 'engagement',
190
- description: 'User rated an item or experience.',
191
- properties: [
192
- prop('value', 'number', 'Rating value', true),
193
- prop('item_id', 'string', 'Identifier of the rated item'),
194
- ],
195
- },
196
- {
197
- name: '$content_view',
198
- displayName: 'Content View',
199
- category: 'engagement',
200
- description: 'User viewed a specific content item.',
201
- properties: [
202
- prop('content_type', 'string', 'Type of content (article, video, product, etc.)'),
203
- prop('content_id', 'string', 'Unique content identifier'),
204
- prop('content_name', 'string', 'Human-readable content name'),
205
- ],
206
- },
207
- // ─── Retention ──────────────────────────────────────────────────────────
208
- {
209
- name: '$app_open',
210
- displayName: 'App Open',
211
- category: 'retention',
212
- description: 'App was opened (not first open).',
213
- properties: [],
214
- },
215
- ];
216
- // ── Lookup helpers ──────────────────────────────────────────────────────────
217
- const _byName = new Map();
218
- for (const ev of exports.CANONICAL_EVENTS)
219
- _byName.set(ev.name, ev);
220
- /** Retrieve a canonical event definition by its `$`-prefixed name. */
221
- function getCanonicalEvent(name) {
222
- return _byName.get(name);
223
- }
224
- /** Set of all canonical event names for quick membership checks. */
225
- exports.CANONICAL_EVENT_NAMES = new Set(exports.CANONICAL_EVENTS.map((e) => e.name));
@@ -1,91 +0,0 @@
1
- /**
2
- * Canonical Event Envelope — Phase 10, W1.1
3
- *
4
- * Single source of truth for the event shape flowing through every ingest
5
- * lane (web SDK, server SDK, future app SDK). Pure module: no DB, no logging,
6
- * no side effects. Must run in Node (ingest routes) and be safe to bundle
7
- * into esbuild for the browser SDK.
8
- *
9
- * Related docs: PHASE-10-ROADMAP.md §W1.1
10
- */
11
- export type EventTier = 'raw' | 'inferred' | 'verified';
12
- export type EventSource = 'client_sdk' | 'server_sdk' | 'system_inferred' | 'user_confirmed';
13
- export type ConsentLevel = 'pending' | 'accepted' | 'rejected';
14
- /**
15
- * Granular consent level for the pseudonymized identity graph.
16
- * - none: anonymous aggregate analytics only, no identity processing
17
- * - analytics: session/pageview/events allowed, anonymous_id claim only
18
- * - marketing: marketing identifiers allowed (email, phone for campaigns)
19
- * - full: all processing including profiling, cross-device, recommendations
20
- */
21
- export type GranularConsentLevel = 'none' | 'analytics' | 'marketing' | 'full';
22
- export interface Envelope {
23
- event_id: string;
24
- timestamp: string;
25
- site_id: string;
26
- anonymous_id: string;
27
- canonical_id?: string;
28
- session_id: string;
29
- event_type: string;
30
- event_name: string;
31
- event_tier: EventTier;
32
- event_source: EventSource;
33
- correlation_id?: string;
34
- parent_event_id?: string;
35
- consent_level: ConsentLevel;
36
- granular_consent_level?: GranularConsentLevel;
37
- sdk_version: string;
38
- config_version?: string;
39
- page_url: string;
40
- page_title?: string;
41
- referrer?: string;
42
- utm_source?: string;
43
- utm_medium?: string;
44
- utm_campaign?: string;
45
- utm_term?: string;
46
- utm_content?: string;
47
- device_type?: string;
48
- browser?: string;
49
- os?: string;
50
- screen_width?: number;
51
- screen_height?: number;
52
- device_id?: string;
53
- phone?: string;
54
- event_source_platform?: 'web' | 'ios' | 'android' | 'react_native' | 'flutter' | 'server';
55
- properties: Record<string, unknown>;
56
- }
57
- export type ParseSuccess = {
58
- ok: true;
59
- value: Envelope;
60
- };
61
- export type ParseError = {
62
- ok: false;
63
- errors: string[];
64
- };
65
- export type ParseResult = ParseSuccess | ParseError;
66
- /**
67
- * Strict parser. Accepts only already-canonical envelopes. Used on internal
68
- * pipeline boundaries where we know producers emit the full shape.
69
- *
70
- * Throws-in-return-value: never throws, returns ParseError on failure.
71
- */
72
- export declare function parseEnvelope(raw: unknown): ParseResult;
73
- /**
74
- * Accepts the loose JSON shape that the current `sdk/tracker.ts` sends and
75
- * promotes it to a canonical Envelope, filling defaults for fields the legacy
76
- * shape doesn't know about. Use this on ingest boundaries during the
77
- * migration window (Phase 10 Wave 1 → Wave 3).
78
- */
79
- export declare function normalizeLegacy(raw: unknown): ParseResult;
80
- export interface CreateEnvelopeInput extends Partial<Envelope> {
81
- site_id: string;
82
- event_name: string;
83
- anonymous_id: string;
84
- session_id: string;
85
- }
86
- /**
87
- * Server-side helper for constructing envelopes from partial input. Fills
88
- * event_id, timestamp, and all enum defaults. Not used on the client SDK hot
89
- * path.
90
- */
91
- export declare function createEnvelope(partial: CreateEnvelopeInput): Envelope;
@@ -1,341 +0,0 @@
1
- "use strict";
2
- /**
3
- * Canonical Event Envelope — Phase 10, W1.1
4
- *
5
- * Single source of truth for the event shape flowing through every ingest
6
- * lane (web SDK, server SDK, future app SDK). Pure module: no DB, no logging,
7
- * no side effects. Must run in Node (ingest routes) and be safe to bundle
8
- * into esbuild for the browser SDK.
9
- *
10
- * Related docs: PHASE-10-ROADMAP.md §W1.1
11
- */
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.parseEnvelope = parseEnvelope;
14
- exports.normalizeLegacy = normalizeLegacy;
15
- exports.createEnvelope = createEnvelope;
16
- const EVENT_TIERS = ['raw', 'inferred', 'verified'];
17
- const EVENT_SOURCES = [
18
- 'client_sdk',
19
- 'server_sdk',
20
- 'system_inferred',
21
- 'user_confirmed',
22
- ];
23
- const CONSENT_LEVELS = [
24
- 'pending',
25
- 'accepted',
26
- 'rejected',
27
- ];
28
- const GRANULAR_CONSENT_LEVELS = [
29
- 'none',
30
- 'analytics',
31
- 'marketing',
32
- 'full',
33
- ];
34
- // ── UUID helper (dependency-free, safe in browser and Node) ─────────────────
35
- function generateUuid() {
36
- // Prefer crypto.randomUUID when available (Node 14.17+, modern browsers).
37
- const g = globalThis.crypto;
38
- if (g && typeof g.randomUUID === 'function') {
39
- try {
40
- return g.randomUUID();
41
- }
42
- catch {
43
- // fall through
44
- }
45
- }
46
- // RFC4122 v4 fallback using Math.random (non-cryptographic but deterministic-free).
47
- const hex = '0123456789abcdef';
48
- let out = '';
49
- for (let i = 0; i < 36; i++) {
50
- if (i === 8 || i === 13 || i === 18 || i === 23) {
51
- out += '-';
52
- }
53
- else if (i === 14) {
54
- out += '4';
55
- }
56
- else if (i === 19) {
57
- out += hex[(Math.random() * 4) | 0 | 8];
58
- }
59
- else {
60
- out += hex[(Math.random() * 16) | 0];
61
- }
62
- }
63
- return out;
64
- }
65
- // ── Field helpers ────────────────────────────────────────────────────────────
66
- function isObject(v) {
67
- return typeof v === 'object' && v !== null && !Array.isArray(v);
68
- }
69
- function asString(v) {
70
- if (typeof v === 'string')
71
- return v;
72
- return undefined;
73
- }
74
- function asNumber(v) {
75
- if (typeof v === 'number' && Number.isFinite(v))
76
- return v;
77
- return undefined;
78
- }
79
- function asProperties(v) {
80
- return isObject(v) ? v : {};
81
- }
82
- function requireString(obj, key, errors) {
83
- const v = obj[key];
84
- if (typeof v !== 'string' || v.length === 0) {
85
- errors.push(`missing or invalid '${key}' (expected non-empty string)`);
86
- return '';
87
- }
88
- return v;
89
- }
90
- function validTimestamp(v, errors) {
91
- if (typeof v === 'string') {
92
- if (!Number.isNaN(Date.parse(v)))
93
- return v;
94
- errors.push(`invalid 'timestamp' (unparseable string)`);
95
- return '';
96
- }
97
- if (typeof v === 'number' && Number.isFinite(v)) {
98
- return new Date(v).toISOString();
99
- }
100
- errors.push(`missing or invalid 'timestamp' (expected ISO-8601 string or epoch ms)`);
101
- return '';
102
- }
103
- function requireEnum(value, allowed, key, errors) {
104
- if (typeof value === 'string' && allowed.includes(value)) {
105
- return value;
106
- }
107
- errors.push(`invalid '${key}': expected one of [${allowed.join(', ')}], got ${JSON.stringify(value)}`);
108
- return allowed[0];
109
- }
110
- // ── parseEnvelope — strict ──────────────────────────────────────────────────
111
- /**
112
- * Strict parser. Accepts only already-canonical envelopes. Used on internal
113
- * pipeline boundaries where we know producers emit the full shape.
114
- *
115
- * Throws-in-return-value: never throws, returns ParseError on failure.
116
- */
117
- function parseEnvelope(raw) {
118
- const errors = [];
119
- if (!isObject(raw)) {
120
- return { ok: false, errors: ['envelope must be an object'] };
121
- }
122
- const event_id = requireString(raw, 'event_id', errors);
123
- const timestamp = validTimestamp(raw.timestamp, errors);
124
- const site_id = requireString(raw, 'site_id', errors);
125
- const anonymous_id = requireString(raw, 'anonymous_id', errors);
126
- const session_id = requireString(raw, 'session_id', errors);
127
- const event_type = requireString(raw, 'event_type', errors);
128
- const event_name = requireString(raw, 'event_name', errors);
129
- const page_url = requireString(raw, 'page_url', errors);
130
- const sdk_version = requireString(raw, 'sdk_version', errors);
131
- const event_tier = requireEnum(raw.event_tier, EVENT_TIERS, 'event_tier', errors);
132
- const event_source = requireEnum(raw.event_source, EVENT_SOURCES, 'event_source', errors);
133
- const consent_level = requireEnum(raw.consent_level, CONSENT_LEVELS, 'consent_level', errors);
134
- // Granular consent is optional — parse if present, skip if absent.
135
- const rawGranular = asString(raw.granular_consent_level);
136
- const granular_consent_level = rawGranular && GRANULAR_CONSENT_LEVELS.includes(rawGranular)
137
- ? rawGranular
138
- : undefined;
139
- if (errors.length > 0) {
140
- return { ok: false, errors };
141
- }
142
- const value = {
143
- event_id,
144
- timestamp,
145
- site_id,
146
- anonymous_id,
147
- canonical_id: asString(raw.canonical_id),
148
- session_id,
149
- event_type,
150
- event_name,
151
- event_tier,
152
- event_source,
153
- correlation_id: asString(raw.correlation_id),
154
- parent_event_id: asString(raw.parent_event_id),
155
- consent_level,
156
- granular_consent_level,
157
- sdk_version,
158
- config_version: asString(raw.config_version),
159
- page_url,
160
- page_title: asString(raw.page_title),
161
- referrer: asString(raw.referrer),
162
- utm_source: asString(raw.utm_source),
163
- utm_medium: asString(raw.utm_medium),
164
- utm_campaign: asString(raw.utm_campaign),
165
- utm_term: asString(raw.utm_term),
166
- utm_content: asString(raw.utm_content),
167
- device_type: asString(raw.device_type),
168
- browser: asString(raw.browser),
169
- os: asString(raw.os),
170
- screen_width: asNumber(raw.screen_width),
171
- screen_height: asNumber(raw.screen_height),
172
- device_id: asString(raw.device_id),
173
- phone: asString(raw.phone),
174
- event_source_platform: asString(raw.event_source_platform),
175
- properties: asProperties(raw.properties),
176
- };
177
- return { ok: true, value };
178
- }
179
- // ── normalizeLegacy — accept current SDK shape ──────────────────────────────
180
- /**
181
- * Maps the legacy SDK consent values ('necessary' | 'analytics' | 'marketing'
182
- * | 'rejected') onto the canonical envelope consent vocabulary. Anything
183
- * non-rejected is treated as 'accepted'; missing values become 'pending'.
184
- */
185
- function mapLegacyConsent(v) {
186
- if (typeof v !== 'string')
187
- return 'pending';
188
- if (v === 'rejected')
189
- return 'rejected';
190
- if (v === 'pending')
191
- return 'pending';
192
- if (v === 'accepted')
193
- return 'accepted';
194
- // 'necessary' | 'analytics' | 'marketing' all indicate affirmative consent.
195
- if (v === 'necessary' || v === 'analytics' || v === 'marketing') {
196
- return 'accepted';
197
- }
198
- return 'pending';
199
- }
200
- /**
201
- * Maps legacy SDK consent values to the 4-level granular consent system.
202
- * - 'rejected' / 'none' → 'none'
203
- * - 'necessary' / 'pending' → 'none' (privacy by default)
204
- * - 'analytics' → 'analytics'
205
- * - 'marketing' → 'marketing'
206
- * - 'accepted' / 'full' → 'full'
207
- * Also accepts an explicit granular_consent_level field if the SDK sends it.
208
- */
209
- function mapLegacyToGranularConsent(raw) {
210
- // If the SDK already sends granular_consent_level, prefer it
211
- const explicit = asString(raw.granular_consent_level);
212
- if (explicit && GRANULAR_CONSENT_LEVELS.includes(explicit)) {
213
- return explicit;
214
- }
215
- // Fall back to mapping the legacy consent_level
216
- const v = asString(raw.consent_level);
217
- if (!v)
218
- return 'none';
219
- switch (v) {
220
- case 'rejected':
221
- case 'none':
222
- case 'necessary':
223
- case 'pending':
224
- return 'none';
225
- case 'analytics':
226
- return 'analytics';
227
- case 'marketing':
228
- return 'marketing';
229
- case 'accepted':
230
- case 'full':
231
- return 'full';
232
- default:
233
- return 'none';
234
- }
235
- }
236
- /**
237
- * Accepts the loose JSON shape that the current `sdk/tracker.ts` sends and
238
- * promotes it to a canonical Envelope, filling defaults for fields the legacy
239
- * shape doesn't know about. Use this on ingest boundaries during the
240
- * migration window (Phase 10 Wave 1 → Wave 3).
241
- */
242
- function normalizeLegacy(raw) {
243
- const errors = [];
244
- if (!isObject(raw)) {
245
- return { ok: false, errors: ['legacy event must be an object'] };
246
- }
247
- // Required in the legacy shape
248
- const anonymous_id = requireString(raw, 'anonymous_id', errors);
249
- const session_id = requireString(raw, 'session_id', errors);
250
- const event_type = requireString(raw, 'event_type', errors);
251
- const event_name = requireString(raw, 'event_name', errors);
252
- if (errors.length > 0) {
253
- return { ok: false, errors };
254
- }
255
- const site_id = asString(raw.site_id) ?? ''; // collect route supplies site_id at the batch level
256
- const event_id = asString(raw.event_id) ?? generateUuid();
257
- const ts = raw.timestamp;
258
- const timestamp = typeof ts === 'string' && !Number.isNaN(Date.parse(ts))
259
- ? ts
260
- : typeof ts === 'number' && Number.isFinite(ts)
261
- ? new Date(ts).toISOString()
262
- : new Date().toISOString();
263
- const value = {
264
- event_id,
265
- timestamp,
266
- site_id,
267
- anonymous_id,
268
- canonical_id: asString(raw.canonical_id),
269
- session_id,
270
- event_type,
271
- event_name,
272
- event_tier: 'raw',
273
- event_source: 'client_sdk',
274
- correlation_id: asString(raw.correlation_id),
275
- parent_event_id: asString(raw.parent_event_id),
276
- consent_level: mapLegacyConsent(raw.consent_level),
277
- granular_consent_level: mapLegacyToGranularConsent(raw),
278
- sdk_version: asString(raw.sdk_version) ?? 'web@legacy',
279
- config_version: asString(raw.config_version),
280
- page_url: asString(raw.page_url) ?? '',
281
- page_title: asString(raw.page_title),
282
- referrer: asString(raw.referrer),
283
- utm_source: asString(raw.utm_source),
284
- utm_medium: asString(raw.utm_medium),
285
- utm_campaign: asString(raw.utm_campaign),
286
- utm_term: asString(raw.utm_term),
287
- utm_content: asString(raw.utm_content),
288
- device_type: asString(raw.device_type),
289
- browser: asString(raw.browser),
290
- os: asString(raw.os),
291
- screen_width: asNumber(raw.screen_width),
292
- screen_height: asNumber(raw.screen_height),
293
- device_id: asString(raw.device_id),
294
- phone: asString(raw.phone),
295
- event_source_platform: asString(raw.event_source_platform),
296
- properties: asProperties(raw.properties),
297
- };
298
- return { ok: true, value };
299
- }
300
- /**
301
- * Server-side helper for constructing envelopes from partial input. Fills
302
- * event_id, timestamp, and all enum defaults. Not used on the client SDK hot
303
- * path.
304
- */
305
- function createEnvelope(partial) {
306
- return {
307
- event_id: partial.event_id ?? generateUuid(),
308
- timestamp: partial.timestamp ?? new Date().toISOString(),
309
- site_id: partial.site_id,
310
- anonymous_id: partial.anonymous_id,
311
- canonical_id: partial.canonical_id,
312
- session_id: partial.session_id,
313
- event_type: partial.event_type ?? 'custom',
314
- event_name: partial.event_name,
315
- event_tier: partial.event_tier ?? 'raw',
316
- event_source: partial.event_source ?? 'client_sdk',
317
- correlation_id: partial.correlation_id,
318
- parent_event_id: partial.parent_event_id,
319
- consent_level: partial.consent_level ?? 'pending',
320
- granular_consent_level: partial.granular_consent_level ?? 'none',
321
- sdk_version: partial.sdk_version ?? 'server@unknown',
322
- config_version: partial.config_version,
323
- page_url: partial.page_url ?? '',
324
- page_title: partial.page_title,
325
- referrer: partial.referrer,
326
- utm_source: partial.utm_source,
327
- utm_medium: partial.utm_medium,
328
- utm_campaign: partial.utm_campaign,
329
- utm_term: partial.utm_term,
330
- utm_content: partial.utm_content,
331
- device_type: partial.device_type,
332
- browser: partial.browser,
333
- os: partial.os,
334
- screen_width: partial.screen_width,
335
- screen_height: partial.screen_height,
336
- device_id: partial.device_id,
337
- phone: partial.phone,
338
- event_source_platform: partial.event_source_platform,
339
- properties: partial.properties ?? {},
340
- };
341
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * @gurulu/shared-core — canonical contracts shared by every Gurulu SDK.
3
- */
4
- export * from './envelope';
5
- export * from './server-event';
6
- export * from './canonical-events';
@@ -1,22 +0,0 @@
1
- "use strict";
2
- /**
3
- * @gurulu/shared-core — canonical contracts shared by every Gurulu SDK.
4
- */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
- };
19
- Object.defineProperty(exports, "__esModule", { value: true });
20
- __exportStar(require("./envelope"), exports);
21
- __exportStar(require("./server-event"), exports);
22
- __exportStar(require("./canonical-events"), exports);
@@ -1,37 +0,0 @@
1
- /**
2
- * Server SDK public types — lifted from packages/node-sdk so every lane
3
- * imports them from a single source of truth.
4
- */
5
- export interface GuruluConfig {
6
- siteId: string;
7
- apiKey: string;
8
- endpoint?: string;
9
- flushInterval?: number;
10
- maxBatchSize?: number;
11
- maxRetries?: number;
12
- debug?: boolean;
13
- }
14
- export interface ServerEvent {
15
- event_name: string;
16
- user_id: string;
17
- properties?: Record<string, unknown>;
18
- timestamp?: string;
19
- idempotency_key?: string;
20
- correlation_id?: string;
21
- }
22
- export interface TrackOptions {
23
- userId: string;
24
- timestamp?: Date;
25
- idempotencyKey?: string;
26
- correlationId?: string;
27
- }
28
- export interface IngestResponse {
29
- accepted: number;
30
- rejected: number;
31
- correlations: Array<{
32
- server_event: string;
33
- matched_client_event: string;
34
- inferred_intent: string;
35
- confidence: number;
36
- }>;
37
- }
@@ -1,6 +0,0 @@
1
- "use strict";
2
- /**
3
- * Server SDK public types — lifted from packages/node-sdk so every lane
4
- * imports them from a single source of truth.
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
File without changes
File without changes