@nexussdk/tracker 0.0.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.
- package/.turbo/turbo-build.log +26 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +355 -0
- package/dist/index.d.ts +355 -0
- package/dist/index.global.js +9 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -0
- package/src/breadcrumbs.ts +156 -0
- package/src/client.ts +289 -0
- package/src/fingerprint.ts +107 -0
- package/src/index.ts +16 -0
- package/src/listeners.ts +49 -0
- package/src/sanitizer.ts +107 -0
- package/src/transport.ts +137 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +19 -0
|
@@ -0,0 +1,107 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
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
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
});
|