@nexussdk/tracker 0.0.1 → 0.0.4

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/src/client.ts DELETED
@@ -1,289 +0,0 @@
1
- /**
2
- * @fileoverview NexusTrackerClient — Full-featured crash ingestion and telemetry SDK.
3
- * Automated global error capture, PII sanitization, deduplication, and hybrid transport.
4
- * @module @nexus/sdk-tracker/client
5
- */
6
-
7
- import type { Breadcrumb, DeviceContext, ErrorEventPayload, UserContext } from '@nexussdk/contracts';
8
- import { resolveApiKey, resolveBaseUrl } from '@nexussdk/core';
9
- import { sanitizeObject } from './sanitizer.js';
10
- import { computeFingerprint, parseStackTrace } from './fingerprint.js';
11
- import { BreadcrumbManager } from './breadcrumbs.js';
12
- import { Transport } from './transport.js';
13
- import { attachGlobalListeners } from './listeners.js';
14
-
15
- /**
16
- * Options for initializing the NexusTrackerClient.
17
- *
18
- * @example
19
- * const tracker = new NexusTrackerClient({
20
- * apiKey: 'pk_live_...',
21
- * environment: 'production',
22
- * autoCapture: true,
23
- * maxBreadcrumbs: 20,
24
- * });
25
- */
26
- export interface NexusTrackerOptions {
27
- /**
28
- * Public API Key ('pk_live_...' or 'pk_test_...').
29
- * If omitted, resolved automatically via env variables.
30
- */
31
- apiKey?: string;
32
- /**
33
- * Centralized Go-Gin Ingestion URL. Defaults to 'https://api.nexus.dev'.
34
- */
35
- baseUrl?: string;
36
- /**
37
- * Target deployment environment ('production' | 'staging' | 'development').
38
- */
39
- environment?: string;
40
- /**
41
- * Max breadcrumbs retained in ring buffer. Defaults to 20 (max 50).
42
- */
43
- maxBreadcrumbs?: number;
44
- /**
45
- * Global tags attached to every captured telemetry event.
46
- */
47
- tags?: Record<string, string>;
48
- /**
49
- * Toggle automated capturing of uncaught exceptions. Defaults to true.
50
- */
51
- autoCapture?: boolean;
52
- /**
53
- * Callback hook to inspect, mutate, or drop an event before dispatch.
54
- * Return null to drop the event completely.
55
- */
56
- beforeSend?: (event: ErrorEventPayload) => ErrorEventPayload | null;
57
- }
58
-
59
- /** Internal deduplication entry. */
60
- interface DedupeEntry {
61
- timer: ReturnType<typeof setTimeout>;
62
- count: number;
63
- lastPayload: ErrorEventPayload;
64
- }
65
-
66
- /**
67
- * Public interface contract for NexusTrackerClient.
68
- */
69
- export interface INexusTrackerClient {
70
- /**
71
- * Manually captures an exception or custom error instance.
72
- *
73
- * @param error - Error object, string message, or unknown rejection value.
74
- * @param extra - Optional custom metadata tags.
75
- *
76
- * @example
77
- * tracker.captureError(new TypeError('Payment failed'), { checkoutStep: 'payment' });
78
- */
79
- captureError(error: unknown, extra?: Record<string, unknown>): void;
80
-
81
- /**
82
- * Records a user activity step into the chronological breadcrumb ring buffer.
83
- *
84
- * @param breadcrumb - Breadcrumb data without timestamp (auto-injected).
85
- *
86
- * @example
87
- * tracker.addBreadcrumb({ category: 'navigation', message: 'Navigated to /checkout', level: 'info' });
88
- */
89
- addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void;
90
-
91
- /**
92
- * Attaches end-user context to subsequent error payloads.
93
- *
94
- * @param user - User context or null to clear.
95
- *
96
- * @example
97
- * tracker.setUser({ id: 'usr_12345', email: 'john@example.com' });
98
- */
99
- setUser(user: UserContext | null): void;
100
-
101
- /**
102
- * Dynamically sets or updates a persistent search tag.
103
- *
104
- * @param key - Tag key name.
105
- * @param value - Tag value string.
106
- *
107
- * @example
108
- * tracker.setTag('app_version', '2.4.1');
109
- */
110
- setTag(key: string, value: string): void;
111
-
112
- /**
113
- * Flushes any buffered events immediately via navigator.sendBeacon or fetch.
114
- *
115
- * @returns Promise that resolves when all events are dispatched.
116
- *
117
- * @example
118
- * await tracker.flush();
119
- */
120
- flush(): Promise<void>;
121
-
122
- /**
123
- * Detaches global window listeners and clears in-memory ring buffers.
124
- *
125
- * @example
126
- * tracker.destroy();
127
- */
128
- destroy(): void;
129
- }
130
-
131
- /**
132
- * NexusTrackerClient — resilient browser crash ingestion agent.
133
- *
134
- * @implements {INexusTrackerClient}
135
- *
136
- * @example
137
- * const tracker = new NexusTrackerClient({ apiKey: 'pk_live_...' });
138
- * tracker.captureError(new Error('Checkout failed'));
139
- */
140
- export class NexusTrackerClient implements INexusTrackerClient {
141
- private readonly apiKey: string;
142
- private readonly environment: string;
143
- private readonly tags: Record<string, string>;
144
- private readonly beforeSend?: (event: ErrorEventPayload) => ErrorEventPayload | null;
145
- private readonly breadcrumbManager: BreadcrumbManager;
146
- private readonly transport: Transport;
147
- private userContext?: UserContext;
148
- private readonly dedupeMap = new Map<string, DedupeEntry>();
149
- private cleanupListeners?: () => void;
150
-
151
- constructor(options: NexusTrackerOptions = {}) {
152
- this.apiKey = resolveApiKey(options.apiKey);
153
- const baseUrl = resolveBaseUrl(options.baseUrl);
154
- this.environment = options.environment ?? 'production';
155
- this.tags = { ...options.tags };
156
- this.beforeSend = options.beforeSend;
157
-
158
- this.breadcrumbManager = new BreadcrumbManager(Math.min(options.maxBreadcrumbs ?? 20, 50));
159
- this.transport = new Transport({
160
- endpoint: `${baseUrl}/api/v1/telemetry/errors`,
161
- apiKey: this.apiKey,
162
- });
163
-
164
- if (options.autoCapture !== false && typeof window !== 'undefined') {
165
- this.breadcrumbManager.attachListeners();
166
- this.cleanupListeners = attachGlobalListeners(this);
167
- }
168
- }
169
-
170
- /**
171
- * Captures an error exception, generates fingerprint, scrubs PII, and enqueues transmission.
172
- */
173
- public captureError(error: unknown, extra?: Record<string, unknown>): void {
174
- const normalized = this.normalizeError(error);
175
- const stackFrames = parseStackTrace(normalized.stack);
176
- const fingerprint = computeFingerprint(normalized.type, normalized.message, stackFrames[0]);
177
-
178
- // Client-side 10-second sliding window deduplication
179
- const existing = this.dedupeMap.get(fingerprint);
180
- if (existing) {
181
- existing.count += 1;
182
- return;
183
- }
184
-
185
- const payload: ErrorEventPayload = {
186
- fingerprint,
187
- errorType: normalized.type,
188
- errorMessage: normalized.message,
189
- stackTrace: stackFrames,
190
- breadcrumbs: this.breadcrumbManager.getAll(),
191
- userContext: this.userContext,
192
- deviceContext: this.getDeviceContext(),
193
- tags: { environment: this.environment, ...this.tags, ...(extra as Record<string, string> | undefined) },
194
- occurrenceCount: 1,
195
- clientTimestamp: Date.now(),
196
- };
197
-
198
- // Apply PII sanitization before transmission
199
- const sanitized = sanitizeObject(payload) as ErrorEventPayload;
200
- const finalPayload = this.beforeSend ? this.beforeSend(sanitized) : sanitized;
201
- if (!finalPayload) return;
202
-
203
- this.transport.send(finalPayload);
204
-
205
- // Track duplicate window — send aggregated event after 10 seconds
206
- const timer = setTimeout(() => {
207
- const entry = this.dedupeMap.get(fingerprint);
208
- if (entry && entry.count > 1) {
209
- const aggregated = { ...entry.lastPayload, occurrenceCount: entry.count, clientTimestamp: Date.now() };
210
- this.transport.send(aggregated);
211
- }
212
- this.dedupeMap.delete(fingerprint);
213
- }, 10_000);
214
-
215
- this.dedupeMap.set(fingerprint, { timer, count: 1, lastPayload: finalPayload });
216
- }
217
-
218
- /**
219
- * Records a contextual breadcrumb in the ring buffer.
220
- */
221
- public addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void {
222
- this.breadcrumbManager.push(breadcrumb);
223
- }
224
-
225
- /**
226
- * Sets or clears the active user context for error telemetry.
227
- */
228
- public setUser(user: UserContext | null): void {
229
- this.userContext = user ?? undefined;
230
- }
231
-
232
- /**
233
- * Sets a custom tag associated with error events.
234
- */
235
- public setTag(key: string, value: string): void {
236
- this.tags[key] = value;
237
- }
238
-
239
- /**
240
- * Flushes all pending deduplication queues and dispatches queued payloads immediately.
241
- */
242
- public async flush(): Promise<void> {
243
- // Flush all pending deduplication timers immediately
244
- for (const [fingerprint, entry] of this.dedupeMap.entries()) {
245
- clearTimeout(entry.timer);
246
- if (entry.count > 0) {
247
- const aggregated = { ...entry.lastPayload, occurrenceCount: entry.count, clientTimestamp: Date.now() };
248
- await this.transport.flush(aggregated);
249
- }
250
- this.dedupeMap.delete(fingerprint);
251
- }
252
- }
253
-
254
- /**
255
- * Tears down global event listeners, flushes queues, and releases resources.
256
- */
257
- public destroy(): void {
258
- this.cleanupListeners?.();
259
- this.breadcrumbManager.detachListeners();
260
- this.breadcrumbManager.clear();
261
- this.dedupeMap.forEach((entry) => clearTimeout(entry.timer));
262
- this.dedupeMap.clear();
263
- }
264
-
265
- private normalizeError(err: unknown): { type: string; message: string; stack?: string } {
266
- if (err instanceof Error) {
267
- return { type: err.name || 'Error', message: err.message, stack: err.stack };
268
- }
269
- if (typeof err === 'string') {
270
- return { type: 'UnhandledException', message: err };
271
- }
272
- // Unknown rejection value (non-Error thrown)
273
- return { type: 'NonErrorRejection', message: String(err) };
274
- }
275
-
276
- private getDeviceContext(): DeviceContext {
277
- const isBrowser = typeof window !== 'undefined' && typeof navigator !== 'undefined';
278
- return {
279
- userAgent: isBrowser ? navigator.userAgent : 'Node/SSR',
280
- currentUrl: isBrowser ? window.location.href : '',
281
- viewport: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined,
282
- timezone: Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone,
283
- networkStatus:
284
- isBrowser && 'connection' in navigator
285
- ? ((navigator as { connection?: { effectiveType?: string } }).connection?.effectiveType ?? 'unknown')
286
- : undefined,
287
- };
288
- }
289
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * @fileoverview Error fingerprinting and stack trace parsing utilities.
3
- * Generates deterministic SHA-256-based fingerprints for error deduplication.
4
- * @module @nexus/sdk-tracker/fingerprint
5
- */
6
-
7
- import type { StackFrame } from '@nexussdk/contracts';
8
-
9
- /**
10
- * Parses a JavaScript error stack string into structured StackFrame objects.
11
- * Supports V8 (Chrome/Node), SpiderMonkey (Firefox), and JavaScriptCore (Safari) formats.
12
- *
13
- * @param stack - Raw stack trace string from an Error object.
14
- * @returns Array of parsed {@link StackFrame} objects (innermost first).
15
- *
16
- * @example
17
- * const frames = parseStackTrace(new TypeError('test').stack);
18
- * // [{ functionName: 'processPayment', fileName: 'chunk.js', lineNumber: 1, columnNumber: 400 }]
19
- */
20
- export function parseStackTrace(stack?: string): StackFrame[] {
21
- if (!stack) return [];
22
-
23
- const frames: StackFrame[] = [];
24
- const lines = stack.split('\n');
25
-
26
- for (const line of lines) {
27
- const trimmed = line.trim();
28
-
29
- // V8 format: " at FunctionName (file.js:line:col)"
30
- // V8 anonymous: " at file.js:line:col"
31
- const v8Match =
32
- trimmed.match(/^at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)$/) ||
33
- trimmed.match(/^at\s+(.+?):(\d+):(\d+)$/) ||
34
- trimmed.match(/^at\s+\((.+?):(\d+):(\d+)\)$/);
35
-
36
- if (v8Match) {
37
- if (v8Match.length === 5) {
38
- // Named function
39
- frames.push({
40
- functionName: v8Match[1] ?? '<anonymous>',
41
- fileName: v8Match[2] ?? '<unknown>',
42
- lineNumber: parseInt(v8Match[3] ?? '0', 10),
43
- columnNumber: parseInt(v8Match[4] ?? '0', 10),
44
- });
45
- } else if (v8Match.length === 4) {
46
- // Anonymous or "at (file:line:col)"
47
- frames.push({
48
- functionName: '<anonymous>',
49
- fileName: v8Match[1] ?? '<unknown>',
50
- lineNumber: parseInt(v8Match[2] ?? '0', 10),
51
- columnNumber: parseInt(v8Match[3] ?? '0', 10),
52
- });
53
- }
54
- continue;
55
- }
56
-
57
- // Firefox/Safari format: "functionName@file.js:line:col"
58
- const geckoMatch = trimmed.match(/^(.+?)@(.+?):(\d+):(\d+)$/);
59
- if (geckoMatch) {
60
- frames.push({
61
- functionName: geckoMatch[1] ?? '<anonymous>',
62
- fileName: geckoMatch[2] ?? '<unknown>',
63
- lineNumber: parseInt(geckoMatch[3] ?? '0', 10),
64
- columnNumber: parseInt(geckoMatch[4] ?? '0', 10),
65
- });
66
- }
67
- }
68
-
69
- return frames;
70
- }
71
-
72
- /**
73
- * Computes a fast non-cryptographic fingerprint string for error deduplication.
74
- * Uses a djb2-style hash over the signature components to avoid SubtleCrypto async API.
75
- *
76
- * Format: hash(errorType + ":" + errorMessage + ":" + topFileName + ":" + topLineNumber)
77
- *
78
- * @param errorType - JavaScript error type (e.g. "TypeError").
79
- * @param errorMessage - Primary error message.
80
- * @param topFrame - Innermost (first) stack frame, or undefined if stack is empty.
81
- * @returns Hex-like fingerprint string for deduplication grouping.
82
- *
83
- * @example
84
- * const fp = computeFingerprint(
85
- * 'TypeError',
86
- * "Cannot read properties of undefined (reading 'map')",
87
- * { functionName: 'render', fileName: 'chunk.js', lineNumber: 1, columnNumber: 400 }
88
- * );
89
- * // 'fp_a3f9b2c1d4...'
90
- */
91
- export function computeFingerprint(
92
- errorType: string,
93
- errorMessage: string,
94
- topFrame?: StackFrame,
95
- ): string {
96
- const fileName = topFrame?.fileName ?? 'unknown';
97
- const lineNumber = topFrame?.lineNumber ?? 0;
98
- const signature = `${errorType}:${errorMessage}:${fileName}:${lineNumber}`;
99
-
100
- // djb2 hash algorithm — fast, no async, no external deps
101
- let hash = 5381;
102
- for (let i = 0; i < signature.length; i++) {
103
- hash = ((hash << 5) + hash + signature.charCodeAt(i)) >>> 0;
104
- }
105
-
106
- return `fp_${hash.toString(16).padStart(8, '0')}`;
107
- }
package/src/index.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * @fileoverview Public export interface for @nexussdk/tracker.
3
- * Re-exports the client, sanitizer, listeners, and all tracker contracts.
4
- *
5
- * @example
6
- * import { NexusTrackerClient } from '@nexussdk/tracker';
7
- * const tracker = new NexusTrackerClient({ apiKey: 'pk_live_...' });
8
- */
9
-
10
- export { NexusTrackerClient } from './client.js';
11
- export type { NexusTrackerOptions, INexusTrackerClient } from './client.js';
12
- export { sanitizeObject, sanitizeUrl } from './sanitizer.js';
13
- export { computeFingerprint, parseStackTrace } from './fingerprint.js';
14
- export { BreadcrumbManager } from './breadcrumbs.js';
15
- export { Transport } from './transport.js';
16
- export type { TransportOptions } from './transport.js';
package/src/listeners.ts DELETED
@@ -1,49 +0,0 @@
1
- /**
2
- * @fileoverview Global error and unhandled rejection listeners.
3
- * Attaches window-level hooks without interfering with native browser behavior.
4
- * @module @nexus/sdk-tracker/listeners
5
- */
6
-
7
- import type { NexusTrackerClient } from './client.js';
8
-
9
- /**
10
- * Attaches global error event listeners to the browser window.
11
- * Captures:
12
- * - `window.onerror` — synchronous uncaught exceptions
13
- * - `window.onunhandledrejection` — unhandled Promise rejections
14
- *
15
- * CRITICAL: Neither handler calls `event.preventDefault()`.
16
- * Native browser behavior (console.error, DevTools display) is preserved.
17
- *
18
- * @param client - The NexusTrackerClient instance to forward errors to.
19
- * @returns Cleanup function that removes all attached listeners.
20
- *
21
- * @example
22
- * const cleanup = attachGlobalListeners(trackerClient);
23
- * // On SDK destroy:
24
- * cleanup();
25
- */
26
- export function attachGlobalListeners(client: NexusTrackerClient): () => void {
27
- if (typeof window === 'undefined') {
28
- return () => void 0; // No-op in SSR contexts
29
- }
30
-
31
- const errorHandler = (event: ErrorEvent): void => {
32
- // Do NOT call event.preventDefault() — preserve native browser behavior
33
- const error = event.error instanceof Error ? event.error : new Error(event.message);
34
- client.captureError(error);
35
- };
36
-
37
- const rejectionHandler = (event: PromiseRejectionEvent): void => {
38
- // Do NOT call event.preventDefault()
39
- client.captureError(event.reason);
40
- };
41
-
42
- window.addEventListener('error', errorHandler);
43
- window.addEventListener('unhandledrejection', rejectionHandler);
44
-
45
- return () => {
46
- window.removeEventListener('error', errorHandler);
47
- window.removeEventListener('unhandledrejection', rejectionHandler);
48
- };
49
- }
package/src/sanitizer.ts DELETED
@@ -1,107 +0,0 @@
1
- /**
2
- * @fileoverview PII sanitization engine for safe error payload transmission.
3
- * Recursively redacts sensitive fields before any data leaves the client device.
4
- * @module @nexus/sdk-tracker/sanitizer
5
- */
6
-
7
- /** Maximum object recursion depth to prevent stack overflows on deep structures. */
8
- const MAX_DEPTH = 5;
9
-
10
- /**
11
- * Regex matching sensitive key names that should be redacted.
12
- * Matches exact key names (case-insensitive) for password, token, secret, etc.
13
- */
14
- const SENSITIVE_KEY_PATTERN =
15
- /^(password|passwd|token|secret|authorization|bearer|auth|credit_?card|cvv|cvc|ssn|api_?key|access_?token|refresh_?token)$/i;
16
-
17
- /**
18
- * Regex matching credit card number patterns (13-16 digit sequences).
19
- * Covers common formats with spaces or dashes between groups.
20
- */
21
- const CREDIT_CARD_PATTERN = /\b(?:\d[ -]*?){13,16}\b/g;
22
-
23
- /**
24
- * Regex matching email addresses in string values.
25
- * Only the domain portion is retained for limited diagnostic context.
26
- */
27
- const EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
28
-
29
- /**
30
- * Regex matching URL query parameters containing sensitive tokens.
31
- * Strips values for: token, auth, key, secret, password, api_key.
32
- */
33
- const SENSITIVE_QUERY_PARAM_PATTERN =
34
- /([?&](token|auth|key|secret|password|api_key|access_token|refresh_token)=)[^&]*/gi;
35
-
36
- /**
37
- * Sanitizes a string value by removing credit card numbers and email addresses.
38
- *
39
- * @param value - The string to sanitize.
40
- * @returns Sanitized string with sensitive patterns replaced.
41
- */
42
- function sanitizeString(value: string): string {
43
- return value
44
- .replace(CREDIT_CARD_PATTERN, '[CARD_REDACTED]')
45
- .replace(EMAIL_PATTERN, '[EMAIL_REDACTED]')
46
- .replace(SENSITIVE_QUERY_PARAM_PATTERN, '$1[REDACTED]');
47
- }
48
-
49
- /**
50
- * Recursively sanitizes an object, redacting sensitive key-value pairs.
51
- * Handles nested objects, arrays, and string values.
52
- *
53
- * @param value - The value to sanitize (any type).
54
- * @param depth - Current recursion depth (internal, starts at 0).
55
- * @returns A sanitized deep copy of the input.
56
- *
57
- * @example
58
- * const payload = {
59
- * user: { email: 'john@example.com', password: 'secret123' },
60
- * token: 'Bearer abc123',
61
- * creditCard: '4111 1111 1111 1111',
62
- * };
63
- * const safe = sanitizeObject(payload);
64
- * // { user: { email: '[EMAIL_REDACTED]', password: '[REDACTED]' },
65
- * // token: '[REDACTED]', creditCard: '[REDACTED]' }
66
- */
67
- export function sanitizeObject(value: unknown, depth = 0): unknown {
68
- if (depth > MAX_DEPTH) return '[MaxDepthExceeded]';
69
-
70
- if (value === null || value === undefined) return value;
71
-
72
- if (typeof value === 'string') {
73
- return sanitizeString(value);
74
- }
75
-
76
- if (typeof value !== 'object') {
77
- return value; // number, boolean, etc.
78
- }
79
-
80
- if (Array.isArray(value)) {
81
- return value.map((item) => sanitizeObject(item, depth + 1));
82
- }
83
-
84
- const sanitized: Record<string, unknown> = {};
85
- for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
86
- if (SENSITIVE_KEY_PATTERN.test(key)) {
87
- sanitized[key] = '[REDACTED]';
88
- } else {
89
- sanitized[key] = sanitizeObject(val, depth + 1);
90
- }
91
- }
92
- return sanitized;
93
- }
94
-
95
- /**
96
- * Sanitizes a URL by stripping sensitive query parameters.
97
- *
98
- * @param url - URL string to sanitize.
99
- * @returns URL with sensitive query values replaced with [REDACTED].
100
- *
101
- * @example
102
- * sanitizeUrl('https://app.com/auth?token=abc123&redirect=/home');
103
- * // 'https://app.com/auth?token=[REDACTED]&redirect=/home'
104
- */
105
- export function sanitizeUrl(url: string): string {
106
- return url.replace(SENSITIVE_QUERY_PARAM_PATTERN, '$1[REDACTED]');
107
- }