@nexussdk/tracker 0.0.1 → 0.0.3
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/README.md +31 -0
- package/dist/index.cjs +1 -2
- package/dist/index.global.js +1 -2
- package/dist/index.mjs +1 -2
- package/package.json +31 -3
- package/.turbo/turbo-build.log +0 -26
- package/dist/index.cjs.map +0 -1
- package/dist/index.global.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/src/breadcrumbs.ts +0 -156
- package/src/client.ts +0 -289
- package/src/fingerprint.ts +0 -107
- package/src/index.ts +0 -16
- package/src/listeners.ts +0 -49
- package/src/sanitizer.ts +0 -107
- package/src/transport.ts +0 -137
- package/tsconfig.json +0 -8
- package/tsup.config.ts +0 -19
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
|
-
}
|
package/src/transport.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Hybrid transport dispatcher for error payload delivery.
|
|
3
|
-
* Prefers fetch with keepalive; falls back to navigator.sendBeacon during page unload.
|
|
4
|
-
* @module @nexus/sdk-tracker/transport
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ErrorEventPayload } from '@nexussdk/contracts';
|
|
8
|
-
import { safeStringify, computeBackoffMs } from '@nexussdk/core';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Options for configuring the transport dispatcher.
|
|
12
|
-
*/
|
|
13
|
-
export interface TransportOptions {
|
|
14
|
-
/** Go-Gin ingestion endpoint URL. */
|
|
15
|
-
endpoint: string;
|
|
16
|
-
/** Public API key for Authorization header. */
|
|
17
|
-
apiKey: string;
|
|
18
|
-
/** Maximum retry attempts on network failures. Defaults to 2. */
|
|
19
|
-
maxRetries?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Hybrid transport dispatcher that intelligently selects the delivery mechanism:
|
|
24
|
-
* - **Normal execution**: `fetch(url, { keepalive: true })` with retry
|
|
25
|
-
* - **Page teardown** (`visibilityState === 'hidden'` or `pagehide`): `navigator.sendBeacon`
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* const transport = new Transport({
|
|
29
|
-
* endpoint: 'http://localhost:8080/api/v1/telemetry/errors',
|
|
30
|
-
* apiKey: 'pk_live_...',
|
|
31
|
-
* });
|
|
32
|
-
* transport.send(errorPayload);
|
|
33
|
-
*/
|
|
34
|
-
export class Transport {
|
|
35
|
-
private readonly options: TransportOptions;
|
|
36
|
-
private readonly maxRetries: number;
|
|
37
|
-
private isPageHiding = false;
|
|
38
|
-
|
|
39
|
-
constructor(options: TransportOptions) {
|
|
40
|
-
this.options = options;
|
|
41
|
-
this.maxRetries = options.maxRetries ?? 2;
|
|
42
|
-
|
|
43
|
-
// Detect page teardown events to switch to sendBeacon
|
|
44
|
-
if (typeof document !== 'undefined') {
|
|
45
|
-
document.addEventListener('visibilitychange', () => {
|
|
46
|
-
if (document.visibilityState === 'hidden') {
|
|
47
|
-
this.isPageHiding = true;
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
if (typeof window !== 'undefined') {
|
|
52
|
-
window.addEventListener('pagehide', () => {
|
|
53
|
-
this.isPageHiding = true;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Dispatches an error payload to the Go-Gin ingestion endpoint.
|
|
60
|
-
* Automatically selects fetch or sendBeacon based on page lifecycle state.
|
|
61
|
-
*
|
|
62
|
-
* @param payload - The sanitized {@link ErrorEventPayload} to transmit.
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* transport.send({
|
|
66
|
-
* fingerprint: 'fp_abc123',
|
|
67
|
-
* errorType: 'TypeError',
|
|
68
|
-
* errorMessage: "Cannot read property 'map' of undefined",
|
|
69
|
-
* // ...
|
|
70
|
-
* });
|
|
71
|
-
*/
|
|
72
|
-
send(payload: ErrorEventPayload): void {
|
|
73
|
-
const body = safeStringify(payload);
|
|
74
|
-
|
|
75
|
-
// Use sendBeacon during page teardown — prevents cancelled fetch requests
|
|
76
|
-
if (
|
|
77
|
-
this.isPageHiding &&
|
|
78
|
-
typeof navigator !== 'undefined' &&
|
|
79
|
-
typeof navigator.sendBeacon === 'function'
|
|
80
|
-
) {
|
|
81
|
-
const blob = new Blob([body], { type: 'application/json' });
|
|
82
|
-
navigator.sendBeacon(this.options.endpoint, blob);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Normal execution: fetch with keepalive and exponential backoff retry
|
|
87
|
-
void this.sendWithRetry(body, 0);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Forces immediate flush of all pending events using sendBeacon.
|
|
92
|
-
* Called during manual flush or SDK destroy lifecycle.
|
|
93
|
-
*
|
|
94
|
-
* @param payload - The error payload to flush.
|
|
95
|
-
* @returns Promise that resolves when the beacon is dispatched.
|
|
96
|
-
*/
|
|
97
|
-
async flush(payload: ErrorEventPayload): Promise<void> {
|
|
98
|
-
const body = safeStringify(payload);
|
|
99
|
-
if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
|
|
100
|
-
const blob = new Blob([body], { type: 'application/json' });
|
|
101
|
-
navigator.sendBeacon(this.options.endpoint, blob);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
await this.sendWithRetry(body, 0);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private async sendWithRetry(body: string, attempt: number): Promise<void> {
|
|
108
|
-
try {
|
|
109
|
-
const response = await fetch(this.options.endpoint, {
|
|
110
|
-
method: 'POST',
|
|
111
|
-
headers: {
|
|
112
|
-
'Content-Type': 'application/json',
|
|
113
|
-
Authorization: `Bearer ${this.options.apiKey}`,
|
|
114
|
-
},
|
|
115
|
-
body,
|
|
116
|
-
keepalive: true,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (response.ok) return;
|
|
120
|
-
|
|
121
|
-
// Retry on 5xx
|
|
122
|
-
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
123
|
-
const delay = computeBackoffMs(attempt, 1000, 30_000);
|
|
124
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
125
|
-
await this.sendWithRetry(body, attempt + 1);
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
// Network failure — retry with backoff
|
|
129
|
-
if (attempt < this.maxRetries) {
|
|
130
|
-
const delay = computeBackoffMs(attempt, 1000, 30_000);
|
|
131
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
132
|
-
await this.sendWithRetry(body, attempt + 1);
|
|
133
|
-
}
|
|
134
|
-
// All retries exhausted — silently drop to prevent host app instability
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
package/tsconfig.json
DELETED
package/tsup.config.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'tsup';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ['src/index.ts'],
|
|
5
|
-
format: ['esm', 'cjs', 'iife'],
|
|
6
|
-
globalName: 'NexusTracker',
|
|
7
|
-
dts: true,
|
|
8
|
-
splitting: false,
|
|
9
|
-
sourcemap: true,
|
|
10
|
-
clean: true,
|
|
11
|
-
minify: true,
|
|
12
|
-
treeshake: true,
|
|
13
|
-
target: 'es2022',
|
|
14
|
-
outExtension({ format }) {
|
|
15
|
-
return {
|
|
16
|
-
js: format === 'esm' ? '.mjs' : format === 'cjs' ? '.cjs' : '.global.js',
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
});
|