@goliapkg/sentori-javascript 0.1.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.
- package/lib/breadcrumbs.d.ts +9 -0
- package/lib/breadcrumbs.d.ts.map +1 -0
- package/lib/breadcrumbs.js +19 -0
- package/lib/breadcrumbs.js.map +1 -0
- package/lib/capture.d.ts +13 -0
- package/lib/capture.d.ts.map +1 -0
- package/lib/capture.js +92 -0
- package/lib/capture.js.map +1 -0
- package/lib/config.d.ts +5 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +11 -0
- package/lib/config.js.map +1 -0
- package/lib/hooks/browser.d.ts +9 -0
- package/lib/hooks/browser.d.ts.map +1 -0
- package/lib/hooks/browser.js +38 -0
- package/lib/hooks/browser.js.map +1 -0
- package/lib/hooks/node.d.ts +14 -0
- package/lib/hooks/node.d.ts.map +1 -0
- package/lib/hooks/node.js +35 -0
- package/lib/hooks/node.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -0
- package/lib/init.d.ts +12 -0
- package/lib/init.d.ts.map +1 -0
- package/lib/init.js +22 -0
- package/lib/init.js.map +1 -0
- package/lib/stack.d.ts +4 -0
- package/lib/stack.d.ts.map +1 -0
- package/lib/stack.js +39 -0
- package/lib/stack.js.map +1 -0
- package/lib/transport.d.ts +17 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +43 -0
- package/lib/transport.js.map +1 -0
- package/lib/types.d.ts +78 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/uuid.d.ts +10 -0
- package/lib/uuid.d.ts.map +1 -0
- package/lib/uuid.js +54 -0
- package/lib/uuid.js.map +1 -0
- package/package.json +54 -0
- package/src/breadcrumbs.ts +27 -0
- package/src/capture.ts +94 -0
- package/src/config.ts +15 -0
- package/src/hooks/browser.ts +43 -0
- package/src/hooks/node.ts +34 -0
- package/src/index.ts +14 -0
- package/src/init.ts +21 -0
- package/src/stack.ts +39 -0
- package/src/transport.ts +58 -0
- package/src/types.ts +70 -0
- package/src/uuid.ts +59 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Breadcrumb, BreadcrumbType } from './types.js';
|
|
2
|
+
export type AddBreadcrumbInput = {
|
|
3
|
+
data?: Record<string, unknown>;
|
|
4
|
+
type: BreadcrumbType;
|
|
5
|
+
};
|
|
6
|
+
export declare function addBreadcrumb(input: AddBreadcrumbInput): void;
|
|
7
|
+
export declare function getBreadcrumbs(): Breadcrumb[];
|
|
8
|
+
export declare function clearBreadcrumbs(): void;
|
|
9
|
+
//# sourceMappingURL=breadcrumbs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breadcrumbs.d.ts","sourceRoot":"","sources":["../src/breadcrumbs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAK5D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,IAAI,EAAE,cAAc,CAAA;CACrB,CAAA;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAQ7D;AAED,wBAAgB,cAAc,IAAI,UAAU,EAAE,CAE7C;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const MAX = 100;
|
|
2
|
+
const buf = [];
|
|
3
|
+
export function addBreadcrumb(input) {
|
|
4
|
+
const crumb = {
|
|
5
|
+
data: input.data ?? {},
|
|
6
|
+
timestamp: new Date().toISOString(),
|
|
7
|
+
type: input.type,
|
|
8
|
+
};
|
|
9
|
+
buf.push(crumb);
|
|
10
|
+
if (buf.length > MAX)
|
|
11
|
+
buf.shift();
|
|
12
|
+
}
|
|
13
|
+
export function getBreadcrumbs() {
|
|
14
|
+
return [...buf];
|
|
15
|
+
}
|
|
16
|
+
export function clearBreadcrumbs() {
|
|
17
|
+
buf.length = 0;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=breadcrumbs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breadcrumbs.js","sourceRoot":"","sources":["../src/breadcrumbs.ts"],"names":[],"mappings":"AAEA,MAAM,GAAG,GAAG,GAAG,CAAA;AACf,MAAM,GAAG,GAAiB,EAAE,CAAA;AAO5B,MAAM,UAAU,aAAa,CAAC,KAAyB;IACrD,MAAM,KAAK,GAAe;QACxB,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;QACtB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,IAAI,EAAE,KAAK,CAAC,IAAI;KACjB,CAAA;IACD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACf,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG;QAAE,GAAG,CAAC,KAAK,EAAE,CAAA;AACnC,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,CAAC,GAAG,GAAG,CAAC,CAAA;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,GAAG,CAAC,MAAM,GAAG,CAAC,CAAA;AAChB,CAAC"}
|
package/lib/capture.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CaptureExtras, User } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Attach a stable user identifier to events captured after this call.
|
|
4
|
+
*
|
|
5
|
+
* PII policy: User shape is `{ id?, anonymous? }` only — no email,
|
|
6
|
+
* name, IP, or other identifying fields. The server schema enforces
|
|
7
|
+
* the same shape; extras would be rejected with `validationFailed`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function setUser(user: User | null): void;
|
|
10
|
+
export declare function getUser(): User | null;
|
|
11
|
+
export declare function captureError(error: Error, extras?: CaptureExtras): void;
|
|
12
|
+
export declare const captureException: typeof captureError;
|
|
13
|
+
//# sourceMappingURL=capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAuB,IAAI,EAAE,MAAM,YAAY,CAAA;AAK1E;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAE/C;AAED,wBAAgB,OAAO,IAAI,IAAI,GAAG,IAAI,CAErC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAmBvE;AAED,eAAO,MAAM,gBAAgB,qBAAe,CAAA"}
|
package/lib/capture.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getBreadcrumbs } from './breadcrumbs.js';
|
|
2
|
+
import { getConfig, isInitialized } from './config.js';
|
|
3
|
+
import { parseStack } from './stack.js';
|
|
4
|
+
import { send } from './transport.js';
|
|
5
|
+
import { uuidV7 } from './uuid.js';
|
|
6
|
+
let _user = null;
|
|
7
|
+
/**
|
|
8
|
+
* Attach a stable user identifier to events captured after this call.
|
|
9
|
+
*
|
|
10
|
+
* PII policy: User shape is `{ id?, anonymous? }` only — no email,
|
|
11
|
+
* name, IP, or other identifying fields. The server schema enforces
|
|
12
|
+
* the same shape; extras would be rejected with `validationFailed`.
|
|
13
|
+
*/
|
|
14
|
+
export function setUser(user) {
|
|
15
|
+
_user = user;
|
|
16
|
+
}
|
|
17
|
+
export function getUser() {
|
|
18
|
+
return _user;
|
|
19
|
+
}
|
|
20
|
+
export function captureError(error, extras) {
|
|
21
|
+
if (!isInitialized())
|
|
22
|
+
return;
|
|
23
|
+
const cfg = getConfig();
|
|
24
|
+
const event = {
|
|
25
|
+
app: { version: parseRelease(cfg.release).version },
|
|
26
|
+
breadcrumbs: getBreadcrumbs(),
|
|
27
|
+
device: detectDevice(),
|
|
28
|
+
environment: cfg.environment,
|
|
29
|
+
error: errorToObject(error),
|
|
30
|
+
fingerprint: extras?.fingerprint,
|
|
31
|
+
id: uuidV7(),
|
|
32
|
+
kind: 'error',
|
|
33
|
+
platform: 'javascript',
|
|
34
|
+
release: cfg.release,
|
|
35
|
+
tags: extras?.tags,
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
user: extras?.user ?? _user,
|
|
38
|
+
};
|
|
39
|
+
void send({ ingestUrl: cfg.ingestUrl, token: cfg.token }, event);
|
|
40
|
+
}
|
|
41
|
+
export const captureException = captureError;
|
|
42
|
+
function errorToObject(error) {
|
|
43
|
+
const causeRaw = error.cause;
|
|
44
|
+
let cause = null;
|
|
45
|
+
if (causeRaw instanceof Error)
|
|
46
|
+
cause = errorToObject(causeRaw);
|
|
47
|
+
return {
|
|
48
|
+
cause,
|
|
49
|
+
message: error.message,
|
|
50
|
+
stack: parseStack(error.stack),
|
|
51
|
+
type: error.name || 'Error',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function parseRelease(release) {
|
|
55
|
+
const m = /^(?:[^@]+@)?([^+]+)(?:\+(.+))?$/.exec(release);
|
|
56
|
+
return { build: m?.[2], version: m?.[1] ?? '0.0.0' };
|
|
57
|
+
}
|
|
58
|
+
function detectDevice() {
|
|
59
|
+
// Browser: light-touch UA sniff. We deliberately avoid full
|
|
60
|
+
// fingerprinting — the field is for grouping context, not analytics.
|
|
61
|
+
const w = globalThis.navigator;
|
|
62
|
+
if (w?.userAgent) {
|
|
63
|
+
return {
|
|
64
|
+
locale: w.language,
|
|
65
|
+
os: detectBrowserOs(w.userAgent),
|
|
66
|
+
osVersion: '0',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Node
|
|
70
|
+
const p = globalThis.process;
|
|
71
|
+
if (p?.platform) {
|
|
72
|
+
return {
|
|
73
|
+
os: p.platform,
|
|
74
|
+
osVersion: p.version?.replace(/^v/, '') ?? '0',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { os: 'unknown', osVersion: '0' };
|
|
78
|
+
}
|
|
79
|
+
function detectBrowserOs(ua) {
|
|
80
|
+
if (ua.includes('Mac OS X') || ua.includes('Macintosh'))
|
|
81
|
+
return 'macos';
|
|
82
|
+
if (ua.includes('Windows'))
|
|
83
|
+
return 'windows';
|
|
84
|
+
if (ua.includes('Linux'))
|
|
85
|
+
return 'linux';
|
|
86
|
+
if (ua.includes('Android'))
|
|
87
|
+
return 'android';
|
|
88
|
+
if (ua.includes('iPhone') || ua.includes('iPad'))
|
|
89
|
+
return 'ios';
|
|
90
|
+
return 'web';
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"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"}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAI7C,wBAAgB,SAAS,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAEhD;AAED,wBAAgB,SAAS,IAAI,WAAW,GAAG,IAAI,CAE9C;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC"}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,IAAI,IAAI,GAAuB,IAAI,CAAA;AAEnC,MAAM,UAAU,SAAS,CAAC,GAAgB;IACxC,IAAI,GAAG,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,KAAK,IAAI,CAAA;AACtB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire window.onerror + unhandledrejection so uncaught browser errors
|
|
3
|
+
* land as Sentori events automatically. Idempotent — safe to call
|
|
4
|
+
* twice; the second call no-ops.
|
|
5
|
+
*/
|
|
6
|
+
export declare function installBrowserHooks(): boolean;
|
|
7
|
+
/** Test helper — resets the idempotency latch. */
|
|
8
|
+
export declare function _resetBrowserHooksForTesting(): void;
|
|
9
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/hooks/browser.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CA4B7C;AAED,kDAAkD;AAClD,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { captureError } from '../capture.js';
|
|
2
|
+
let installed = false;
|
|
3
|
+
/**
|
|
4
|
+
* Wire window.onerror + unhandledrejection so uncaught browser errors
|
|
5
|
+
* land as Sentori events automatically. Idempotent — safe to call
|
|
6
|
+
* twice; the second call no-ops.
|
|
7
|
+
*/
|
|
8
|
+
export function installBrowserHooks() {
|
|
9
|
+
if (installed)
|
|
10
|
+
return true;
|
|
11
|
+
const w = globalThis;
|
|
12
|
+
if (typeof w.addEventListener !== 'function')
|
|
13
|
+
return false;
|
|
14
|
+
const onError = (e) => {
|
|
15
|
+
const err = e.error;
|
|
16
|
+
if (err instanceof Error)
|
|
17
|
+
captureError(err);
|
|
18
|
+
else if (typeof e.message === 'string') {
|
|
19
|
+
captureError(new Error(e.message));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const onRejection = (e) => {
|
|
23
|
+
const reason = e.reason;
|
|
24
|
+
if (reason instanceof Error)
|
|
25
|
+
captureError(reason);
|
|
26
|
+
else
|
|
27
|
+
captureError(new Error(typeof reason === 'string' ? reason : 'unhandled rejection'));
|
|
28
|
+
};
|
|
29
|
+
w.addEventListener('error', onError);
|
|
30
|
+
w.addEventListener('unhandledrejection', onRejection);
|
|
31
|
+
installed = true;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
/** Test helper — resets the idempotency latch. */
|
|
35
|
+
export function _resetBrowserHooksForTesting() {
|
|
36
|
+
installed = false;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/hooks/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,IAAI,SAAS,GAAG,KAAK,CAAA;AAErB;;;;GAIG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,SAAS;QAAE,OAAO,IAAI,CAAA;IAC1B,MAAM,CAAC,GAAG,UAKT,CAAA;IACD,IAAI,OAAO,CAAC,CAAC,gBAAgB,KAAK,UAAU;QAAE,OAAO,KAAK,CAAA;IAE1D,MAAM,OAAO,GAAG,CAAC,CAAqB,EAAE,EAAE;QACxC,MAAM,GAAG,GAAI,CAAgB,CAAC,KAAK,CAAA;QACnC,IAAI,GAAG,YAAY,KAAK;YAAE,YAAY,CAAC,GAAG,CAAC,CAAA;aACtC,IAAI,OAAQ,CAAgB,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACvD,YAAY,CAAC,IAAI,KAAK,CAAE,CAAgB,CAAC,OAAO,CAAC,CAAC,CAAA;QACpD,CAAC;IACH,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,CAAC,CAAgC,EAAE,EAAE;QACvD,MAAM,MAAM,GAAI,CAA2B,CAAC,MAAM,CAAA;QAClD,IAAI,MAAM,YAAY,KAAK;YAAE,YAAY,CAAC,MAAM,CAAC,CAAA;;YAC5C,YAAY,CAAC,IAAI,KAAK,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAA;IAC3F,CAAC,CAAA;IAED,CAAC,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IACpC,CAAC,CAAC,gBAAgB,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAA;IACrD,SAAS,GAAG,IAAI,CAAA;IAChB,OAAO,IAAI,CAAA;AACb,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,4BAA4B;IAC1C,SAAS,GAAG,KAAK,CAAA;AACnB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire process.on('uncaughtException') + 'unhandledRejection'.
|
|
3
|
+
* Idempotent. Returns false if not running on Node (no `process.on`).
|
|
4
|
+
*
|
|
5
|
+
* Node policy notes:
|
|
6
|
+
* - We do NOT call process.exit on uncaughtException; Sentori doesn't
|
|
7
|
+
* own the host's crash strategy. The host's existing handler
|
|
8
|
+
* (default: log + exit 1) runs after ours.
|
|
9
|
+
* - Bun + Deno expose process.on for compatibility; the same code
|
|
10
|
+
* path covers them.
|
|
11
|
+
*/
|
|
12
|
+
export declare function installNodeHooks(): boolean;
|
|
13
|
+
export declare function _resetNodeHooksForTesting(): void;
|
|
14
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/hooks/node.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAc1C;AAED,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { captureError } from '../capture.js';
|
|
2
|
+
let installed = false;
|
|
3
|
+
/**
|
|
4
|
+
* Wire process.on('uncaughtException') + 'unhandledRejection'.
|
|
5
|
+
* Idempotent. Returns false if not running on Node (no `process.on`).
|
|
6
|
+
*
|
|
7
|
+
* Node policy notes:
|
|
8
|
+
* - We do NOT call process.exit on uncaughtException; Sentori doesn't
|
|
9
|
+
* own the host's crash strategy. The host's existing handler
|
|
10
|
+
* (default: log + exit 1) runs after ours.
|
|
11
|
+
* - Bun + Deno expose process.on for compatibility; the same code
|
|
12
|
+
* path covers them.
|
|
13
|
+
*/
|
|
14
|
+
export function installNodeHooks() {
|
|
15
|
+
if (installed)
|
|
16
|
+
return true;
|
|
17
|
+
const p = globalThis.process;
|
|
18
|
+
if (!p || typeof p.on !== 'function')
|
|
19
|
+
return false;
|
|
20
|
+
p.on('uncaughtException', (err) => {
|
|
21
|
+
captureError(err);
|
|
22
|
+
});
|
|
23
|
+
p.on('unhandledRejection', (reason) => {
|
|
24
|
+
if (reason instanceof Error)
|
|
25
|
+
captureError(reason);
|
|
26
|
+
else
|
|
27
|
+
captureError(new Error(typeof reason === 'string' ? reason : 'unhandled rejection'));
|
|
28
|
+
});
|
|
29
|
+
installed = true;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
export function _resetNodeHooksForTesting() {
|
|
33
|
+
installed = false;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../src/hooks/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,IAAI,SAAS,GAAG,KAAK,CAAA;AAErB;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB;IAC9B,IAAI,SAAS;QAAE,OAAO,IAAI,CAAA;IAC1B,MAAM,CAAC,GAAI,UAA2C,CAAC,OAAO,CAAA;IAC9D,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,UAAU;QAAE,OAAO,KAAK,CAAA;IAElD,CAAC,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,GAAU,EAAE,EAAE;QACvC,YAAY,CAAC,GAAG,CAAC,CAAA;IACnB,CAAC,CAAC,CAAA;IACF,CAAC,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,MAAe,EAAE,EAAE;QAC7C,IAAI,MAAM,YAAY,KAAK;YAAE,YAAY,CAAC,MAAM,CAAC,CAAA;;YAC5C,YAAY,CAAC,IAAI,KAAK,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAA;IAC3F,CAAC,CAAC,CAAA;IACF,SAAS,GAAG,IAAI,CAAA;IAChB,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,yBAAyB;IACvC,SAAS,GAAG,KAAK,CAAA;AACnB,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { addBreadcrumb, clearBreadcrumbs, getBreadcrumbs } from './breadcrumbs.js';
|
|
2
|
+
export { captureError, captureException, getUser, setUser } from './capture.js';
|
|
3
|
+
export { initSentori } from './init.js';
|
|
4
|
+
export type { Breadcrumb, BreadcrumbType, CaptureExtras, Event, Frame, InitOptions, SentoriError, Tags, User, } from './types.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAClF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,YAAY,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,KAAK,EACL,KAAK,EACL,WAAW,EACX,YAAY,EACZ,IAAI,EACJ,IAAI,GACL,MAAM,YAAY,CAAA"}
|
package/lib/index.js
ADDED
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAClF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA"}
|
package/lib/init.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { InitOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configure the SDK and (by default) wire global error handlers.
|
|
4
|
+
*
|
|
5
|
+
* Browser: window 'error' + 'unhandledrejection' → captureError.
|
|
6
|
+
* Node: process 'uncaughtException' + 'unhandledRejection' → captureError.
|
|
7
|
+
*
|
|
8
|
+
* Pass `enableGlobalHooks: false` if you want to drive captures
|
|
9
|
+
* manually (e.g. tests, or a host that owns its own crash plumbing).
|
|
10
|
+
*/
|
|
11
|
+
export declare function initSentori(options: InitOptions): void;
|
|
12
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAMtD"}
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { setConfig } from './config.js';
|
|
2
|
+
import { installBrowserHooks } from './hooks/browser.js';
|
|
3
|
+
import { installNodeHooks } from './hooks/node.js';
|
|
4
|
+
/**
|
|
5
|
+
* Configure the SDK and (by default) wire global error handlers.
|
|
6
|
+
*
|
|
7
|
+
* Browser: window 'error' + 'unhandledrejection' → captureError.
|
|
8
|
+
* Node: process 'uncaughtException' + 'unhandledRejection' → captureError.
|
|
9
|
+
*
|
|
10
|
+
* Pass `enableGlobalHooks: false` if you want to drive captures
|
|
11
|
+
* manually (e.g. tests, or a host that owns its own crash plumbing).
|
|
12
|
+
*/
|
|
13
|
+
export function initSentori(options) {
|
|
14
|
+
setConfig(options);
|
|
15
|
+
if (options.enableGlobalHooks === false)
|
|
16
|
+
return;
|
|
17
|
+
// Browser comes first because both globals can exist in some
|
|
18
|
+
// bundlers' shims; we want browser semantics on the web.
|
|
19
|
+
if (!installBrowserHooks())
|
|
20
|
+
installNodeHooks();
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=init.js.map
|
package/lib/init.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAGlD;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,OAAoB;IAC9C,SAAS,CAAC,OAAO,CAAC,CAAA;IAClB,IAAI,OAAO,CAAC,iBAAiB,KAAK,KAAK;QAAE,OAAM;IAC/C,6DAA6D;IAC7D,yDAAyD;IACzD,IAAI,CAAC,mBAAmB,EAAE;QAAE,gBAAgB,EAAE,CAAA;AAChD,CAAC"}
|
package/lib/stack.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack.d.ts","sourceRoot":"","sources":["../src/stack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AASvC,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,EAAE,CAoB7D"}
|
package/lib/stack.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// V8 / Node / Bun: "at fn (file:line:col)" or "at file:line:col" or "at fn file:line:col"
|
|
2
|
+
// File can be a URL (https://…), so we anchor on the trailing `:\d+:\d+\)?$`
|
|
3
|
+
// and let `(?<file>.+?)` swallow whatever comes before.
|
|
4
|
+
const V8_RE = /^\s*at\s+(?:(?<fn>.+?)\s+)?\(?(?<file>.+?):(?<line>\d+):(?<col>\d+)\)?\s*$/;
|
|
5
|
+
// SpiderMonkey / Safari: "fn@file:line:col" — same trailing anchor.
|
|
6
|
+
const SPIDER_RE = /^(?:(?<fn>[^@]*)@)?(?<file>.+?):(?<line>\d+):(?<col>\d+)\s*$/;
|
|
7
|
+
/** Best-effort parse of an `Error.stack` string into Sentori frames. */
|
|
8
|
+
export function parseStack(stack) {
|
|
9
|
+
if (!stack)
|
|
10
|
+
return [];
|
|
11
|
+
const lines = stack.split('\n');
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const raw of lines) {
|
|
14
|
+
const line = raw.trim();
|
|
15
|
+
if (!line)
|
|
16
|
+
continue;
|
|
17
|
+
const m = V8_RE.exec(line) ?? SPIDER_RE.exec(line);
|
|
18
|
+
if (!m?.groups)
|
|
19
|
+
continue;
|
|
20
|
+
const file = m.groups.file ?? '<anonymous>';
|
|
21
|
+
out.push({
|
|
22
|
+
absolutePath: file,
|
|
23
|
+
column: Number(m.groups.col),
|
|
24
|
+
file: shortFile(file),
|
|
25
|
+
function: m.groups.fn?.trim(),
|
|
26
|
+
inApp: !file.includes('node_modules') && !file.startsWith('node:'),
|
|
27
|
+
line: Number(m.groups.line),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function shortFile(absolute) {
|
|
33
|
+
// Strip protocol + leading path noise so the dashboard shows
|
|
34
|
+
// e.g. "App.tsx" instead of "https://example.com/static/App.tsx".
|
|
35
|
+
const noProto = absolute.replace(/^https?:\/\/[^/]+\//, '');
|
|
36
|
+
const tail = noProto.split('/').slice(-2).join('/');
|
|
37
|
+
return tail || absolute;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=stack.js.map
|
package/lib/stack.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack.js","sourceRoot":"","sources":["../src/stack.ts"],"names":[],"mappings":"AAEA,0FAA0F;AAC1F,6EAA6E;AAC7E,wDAAwD;AACxD,MAAM,KAAK,GAAG,4EAA4E,CAAA;AAC1F,oEAAoE;AACpE,MAAM,SAAS,GAAG,8DAA8D,CAAA;AAEhF,wEAAwE;AACxE,MAAM,UAAU,UAAU,CAAC,KAAyB;IAClD,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAA;IACrB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC/B,MAAM,GAAG,GAAY,EAAE,CAAA;IACvB,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;QACvB,IAAI,CAAC,IAAI;YAAE,SAAQ;QACnB,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClD,IAAI,CAAC,CAAC,EAAE,MAAM;YAAE,SAAQ;QACxB,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,aAAa,CAAA;QAC3C,GAAG,CAAC,IAAI,CAAC;YACP,YAAY,EAAE,IAAI;YAClB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;YAC5B,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC;YACrB,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE;YAC7B,KAAK,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAClE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;SAC5B,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB;IACjC,6DAA6D;IAC7D,kEAAkE;IAClE,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAA;IAC3D,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACnD,OAAO,IAAI,IAAI,QAAQ,CAAA;AACzB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Event } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal HTTP transport. POST /v1/events with a Bearer token.
|
|
4
|
+
* - Browser: prefers `navigator.sendBeacon` on page-unload paths;
|
|
5
|
+
* otherwise plain fetch with `keepalive: true` so events survive
|
|
6
|
+
* a tab close mid-flight.
|
|
7
|
+
* - Node: plain fetch (Node 18+ has it global).
|
|
8
|
+
*
|
|
9
|
+
* On 4xx/5xx the SDK currently drops the event silently — retry +
|
|
10
|
+
* persistent queue is a follow-up if anyone actually wants it.
|
|
11
|
+
*/
|
|
12
|
+
export type TransportConfig = {
|
|
13
|
+
ingestUrl: string;
|
|
14
|
+
token: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function send(cfg: TransportConfig, event: Event): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,wBAAsB,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAwC5E"}
|
package/lib/transport.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export async function send(cfg, event) {
|
|
2
|
+
const url = `${cfg.ingestUrl.replace(/\/+$/, '')}/v1/events`;
|
|
3
|
+
const body = JSON.stringify(event);
|
|
4
|
+
const headers = {
|
|
5
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
6
|
+
'Content-Type': 'application/json',
|
|
7
|
+
'Sentori-Sdk': 'sentori-javascript/0.1.0',
|
|
8
|
+
};
|
|
9
|
+
// Browser: navigator.sendBeacon is fire-and-forget and survives
|
|
10
|
+
// tab close. Bound by user-agent quotas (~64KB), so we feature-detect
|
|
11
|
+
// and only use it for small bodies.
|
|
12
|
+
const beacon = globalThis
|
|
13
|
+
.navigator?.sendBeacon;
|
|
14
|
+
if (typeof beacon === 'function' && body.length < 60_000) {
|
|
15
|
+
try {
|
|
16
|
+
const blob = new Blob([body], { type: 'application/json' });
|
|
17
|
+
// sendBeacon doesn't carry headers — Authorization moves into
|
|
18
|
+
// a query param so the server's existing Bearer auth still works.
|
|
19
|
+
const beaconUrl = `${url}?token=${encodeURIComponent(cfg.token)}`;
|
|
20
|
+
if (beacon.call(globalThis.navigator, beaconUrl, blob))
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// fall through to fetch
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await fetch(url, {
|
|
29
|
+
body,
|
|
30
|
+
headers,
|
|
31
|
+
keepalive: true,
|
|
32
|
+
method: 'POST',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
// No retry — log and forget. Hosts that care can wrap and add
|
|
37
|
+
// their own retry policy at the app layer.
|
|
38
|
+
if (typeof console !== 'undefined') {
|
|
39
|
+
console.warn('[sentori] transport failed:', e.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAiBA,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,GAAoB,EAAE,KAAY;IAC3D,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,YAAY,CAAA;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAClC,MAAM,OAAO,GAAG;QACd,aAAa,EAAE,UAAU,GAAG,CAAC,KAAK,EAAE;QACpC,cAAc,EAAE,kBAAkB;QAClC,aAAa,EAAE,0BAA0B;KAC1C,CAAA;IAED,gEAAgE;IAChE,sEAAsE;IACtE,oCAAoC;IACpC,MAAM,MAAM,GAAI,UAA+E;SAC5F,SAAS,EAAE,UAAU,CAAA;IACxB,IAAI,OAAO,MAAM,KAAK,UAAU,IAAI,IAAI,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC;QACzD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,CAAA;YAC3D,8DAA8D;YAC9D,kEAAkE;YAClE,MAAM,SAAS,GAAG,GAAG,GAAG,UAAU,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAA;YACjE,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC;gBAAE,OAAM;QAChE,CAAC;QAAC,MAAM,CAAC;YACP,wBAAwB;QAC1B,CAAC;IACH,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,GAAG,EAAE;YACf,IAAI;YACJ,OAAO;YACP,SAAS,EAAE,IAAI;YACf,MAAM,EAAE,MAAM;SACf,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,8DAA8D;QAC9D,2CAA2C;QAC3C,IAAI,OAAO,OAAO,KAAK,WAAW,EAAE,CAAC;YACnC,OAAO,CAAC,IAAI,CAAC,6BAA6B,EAAG,CAAW,CAAC,OAAO,CAAC,CAAA;QACnE,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire shape for an event sent to the Sentori `/v1/events` endpoint.
|
|
3
|
+
* Identical to the protocol documented in `docs/protocol.md` and the
|
|
4
|
+
* server's `event::Event` Rust type.
|
|
5
|
+
*/
|
|
6
|
+
export type Event = {
|
|
7
|
+
app: {
|
|
8
|
+
build?: string;
|
|
9
|
+
framework?: {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
};
|
|
13
|
+
version: string;
|
|
14
|
+
};
|
|
15
|
+
breadcrumbs: Breadcrumb[];
|
|
16
|
+
device: {
|
|
17
|
+
locale?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
os: string;
|
|
20
|
+
osVersion: string;
|
|
21
|
+
};
|
|
22
|
+
environment: string;
|
|
23
|
+
error: SentoriError;
|
|
24
|
+
fingerprint?: string[];
|
|
25
|
+
id: string;
|
|
26
|
+
kind: 'error';
|
|
27
|
+
platform: 'javascript';
|
|
28
|
+
release: string;
|
|
29
|
+
spanId?: null | string;
|
|
30
|
+
tags?: Tags;
|
|
31
|
+
timestamp: string;
|
|
32
|
+
traceId?: null | string;
|
|
33
|
+
user?: null | User;
|
|
34
|
+
};
|
|
35
|
+
export type SentoriError = {
|
|
36
|
+
cause: null | SentoriError;
|
|
37
|
+
message: string;
|
|
38
|
+
stack: Frame[];
|
|
39
|
+
type: string;
|
|
40
|
+
};
|
|
41
|
+
export type Frame = {
|
|
42
|
+
absolutePath?: string;
|
|
43
|
+
column?: number;
|
|
44
|
+
file: string;
|
|
45
|
+
function?: string;
|
|
46
|
+
inApp: boolean;
|
|
47
|
+
line: number;
|
|
48
|
+
};
|
|
49
|
+
export type BreadcrumbType = 'custom' | 'log' | 'nav' | 'net' | 'user';
|
|
50
|
+
export type Breadcrumb = {
|
|
51
|
+
data: Record<string, unknown>;
|
|
52
|
+
timestamp: string;
|
|
53
|
+
type: BreadcrumbType;
|
|
54
|
+
};
|
|
55
|
+
/** PII-minimal — same shape as the RN SDK and the server schema. */
|
|
56
|
+
export type User = {
|
|
57
|
+
anonymous?: boolean;
|
|
58
|
+
id?: string;
|
|
59
|
+
};
|
|
60
|
+
export type Tags = Record<string, string>;
|
|
61
|
+
export type CaptureExtras = {
|
|
62
|
+
fingerprint?: string[];
|
|
63
|
+
tags?: Tags;
|
|
64
|
+
user?: User;
|
|
65
|
+
};
|
|
66
|
+
export type InitOptions = {
|
|
67
|
+
/** Override automatic global hooks. Default: true on browser + node. */
|
|
68
|
+
enableGlobalHooks?: boolean;
|
|
69
|
+
/** "prod" / "dev" / "staging" / whatever you want. */
|
|
70
|
+
environment: string;
|
|
71
|
+
/** e.g. https://ingest.sentori.golia.jp */
|
|
72
|
+
ingestUrl: string;
|
|
73
|
+
/** e.g. "myapp@1.2.3+456" */
|
|
74
|
+
release: string;
|
|
75
|
+
/** st_pk_<26 base32 chars> */
|
|
76
|
+
token: string;
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG;IAClB,GAAG,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACvF,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1E,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,YAAY,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;IACtB,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;IACvB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,IAAI,GAAG,YAAY,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,KAAK,EAAE,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,OAAO,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAA;AAEtE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,cAAc,CAAA;CACrB,CAAA;AAED,oEAAoE;AACpE,MAAM,MAAM,IAAI,GAAG;IAAE,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvD,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEzC,MAAM,MAAM,aAAa,GAAG;IAC1B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,IAAI,CAAC,EAAE,IAAI,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,wEAAwE;IACxE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAA;IACjB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAA;CACd,CAAA"}
|
package/lib/types.js
ADDED
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/lib/uuid.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uuid v7 (timestamp-prefixed). Modern Node ≥ 19 + browsers expose
|
|
3
|
+
* `crypto.randomUUID()` for v4 — that gives us the entropy half;
|
|
4
|
+
* v7 layout is cheap to assemble manually.
|
|
5
|
+
*
|
|
6
|
+
* Layout (RFC 9562 v7):
|
|
7
|
+
* ms (48 bits) | ver=7 (4) | rand_a (12) | var=10 (2) | rand_b (62)
|
|
8
|
+
*/
|
|
9
|
+
export declare function uuidV7(): string;
|
|
10
|
+
//# sourceMappingURL=uuid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../src/uuid.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,MAAM,IAAI,MAAM,CAgC/B"}
|
package/lib/uuid.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uuid v7 (timestamp-prefixed). Modern Node ≥ 19 + browsers expose
|
|
3
|
+
* `crypto.randomUUID()` for v4 — that gives us the entropy half;
|
|
4
|
+
* v7 layout is cheap to assemble manually.
|
|
5
|
+
*
|
|
6
|
+
* Layout (RFC 9562 v7):
|
|
7
|
+
* ms (48 bits) | ver=7 (4) | rand_a (12) | var=10 (2) | rand_b (62)
|
|
8
|
+
*/
|
|
9
|
+
export function uuidV7() {
|
|
10
|
+
const ms = Date.now();
|
|
11
|
+
const rand = new Uint8Array(10);
|
|
12
|
+
cryptoRandomFill(rand);
|
|
13
|
+
// 6 bytes of timestamp (ms), big-endian
|
|
14
|
+
const t = new Uint8Array(6);
|
|
15
|
+
let n = ms;
|
|
16
|
+
for (let i = 5; i >= 0; i--) {
|
|
17
|
+
t[i] = n & 0xff;
|
|
18
|
+
n = Math.floor(n / 256);
|
|
19
|
+
}
|
|
20
|
+
// pack version + variant
|
|
21
|
+
rand[0] = (rand[0] & 0x0f) | 0x70; // version 7 in high nibble of byte 6
|
|
22
|
+
rand[2] = (rand[2] & 0x3f) | 0x80; // variant 10 in high two bits of byte 8
|
|
23
|
+
const bytes = new Uint8Array(16);
|
|
24
|
+
bytes.set(t, 0);
|
|
25
|
+
bytes.set(rand, 6);
|
|
26
|
+
return (hex(bytes.subarray(0, 4)) +
|
|
27
|
+
'-' +
|
|
28
|
+
hex(bytes.subarray(4, 6)) +
|
|
29
|
+
'-' +
|
|
30
|
+
hex(bytes.subarray(6, 8)) +
|
|
31
|
+
'-' +
|
|
32
|
+
hex(bytes.subarray(8, 10)) +
|
|
33
|
+
'-' +
|
|
34
|
+
hex(bytes.subarray(10, 16)));
|
|
35
|
+
}
|
|
36
|
+
function cryptoRandomFill(buf) {
|
|
37
|
+
// Browser + Node 19+ + Bun all expose globalThis.crypto.
|
|
38
|
+
const c = globalThis.crypto;
|
|
39
|
+
if (c?.getRandomValues) {
|
|
40
|
+
c.getRandomValues(buf);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Last-resort Math.random (only hits in very old envs; entropy
|
|
44
|
+
// quality is bad but we still want a unique-ish id).
|
|
45
|
+
for (let i = 0; i < buf.length; i++)
|
|
46
|
+
buf[i] = Math.floor(Math.random() * 256);
|
|
47
|
+
}
|
|
48
|
+
function hex(b) {
|
|
49
|
+
let s = '';
|
|
50
|
+
for (const x of b)
|
|
51
|
+
s += x.toString(16).padStart(2, '0');
|
|
52
|
+
return s;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=uuid.js.map
|
package/lib/uuid.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.js","sourceRoot":"","sources":["../src/uuid.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,MAAM;IACpB,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACrB,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAA;IAC/B,gBAAgB,CAAC,IAAI,CAAC,CAAA;IAEtB,wCAAwC;IACxC,MAAM,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAA;IAC3B,IAAI,CAAC,GAAG,EAAE,CAAA;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAAA;QACf,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,CAAA;IACzB,CAAC;IAED,yBAAyB;IACzB,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAA,CAAC,qCAAqC;IACxE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAA,CAAC,wCAAwC;IAE3E,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAA;IAChC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACf,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IAElB,OAAO,CACL,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACzB,GAAG;QACH,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACzB,GAAG;QACH,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACzB,GAAG;QACH,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1B,GAAG;QACH,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAC5B,CAAA;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAe;IACvC,yDAAyD;IACzD,MAAM,CAAC,GAAI,UAAyE,CAAC,MAAM,CAAA;IAC3F,IAAI,CAAC,EAAE,eAAe,EAAE,CAAC;QACvB,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;QACtB,OAAM;IACR,CAAC;IACD,+DAA+D;IAC/D,qDAAqD;IACrD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAA;AAC/E,CAAC;AAED,SAAS,GAAG,CAAC,CAAa;IACxB,IAAI,CAAC,GAAG,EAAE,CAAA;IACV,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IACvD,OAAO,CAAC,CAAA;AACV,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goliapkg/sentori-javascript",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sentori SDK for browsers and Node — error capture, breadcrumbs, and HTTP transport. Same wire protocol as @goliapkg/sentori-react-native.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://sentori.golia.jp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/goliajp/sentori.git",
|
|
10
|
+
"directory": "sdk/javascript"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/goliajp/sentori/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"sentori",
|
|
17
|
+
"error-tracking",
|
|
18
|
+
"crash-reporting",
|
|
19
|
+
"monitoring",
|
|
20
|
+
"javascript",
|
|
21
|
+
"browser",
|
|
22
|
+
"node"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "lib/index.js",
|
|
26
|
+
"types": "lib/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./lib/index.d.ts",
|
|
30
|
+
"import": "./lib/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/",
|
|
35
|
+
"src/",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.json",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "bun test",
|
|
42
|
+
"prepack": "bun run build"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/bun": "latest",
|
|
46
|
+
"typescript": "^5"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Breadcrumb, BreadcrumbType } from './types.js'
|
|
2
|
+
|
|
3
|
+
const MAX = 100
|
|
4
|
+
const buf: Breadcrumb[] = []
|
|
5
|
+
|
|
6
|
+
export type AddBreadcrumbInput = {
|
|
7
|
+
data?: Record<string, unknown>
|
|
8
|
+
type: BreadcrumbType
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function addBreadcrumb(input: AddBreadcrumbInput): void {
|
|
12
|
+
const crumb: Breadcrumb = {
|
|
13
|
+
data: input.data ?? {},
|
|
14
|
+
timestamp: new Date().toISOString(),
|
|
15
|
+
type: input.type,
|
|
16
|
+
}
|
|
17
|
+
buf.push(crumb)
|
|
18
|
+
if (buf.length > MAX) buf.shift()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getBreadcrumbs(): Breadcrumb[] {
|
|
22
|
+
return [...buf]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function clearBreadcrumbs(): void {
|
|
26
|
+
buf.length = 0
|
|
27
|
+
}
|
package/src/capture.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { getBreadcrumbs } from './breadcrumbs.js'
|
|
2
|
+
import { getConfig, isInitialized } from './config.js'
|
|
3
|
+
import { parseStack } from './stack.js'
|
|
4
|
+
import { send } from './transport.js'
|
|
5
|
+
import type { CaptureExtras, Event, SentoriError, User } from './types.js'
|
|
6
|
+
import { uuidV7 } from './uuid.js'
|
|
7
|
+
|
|
8
|
+
let _user: User | null = null
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Attach a stable user identifier to events captured after this call.
|
|
12
|
+
*
|
|
13
|
+
* PII policy: User shape is `{ id?, anonymous? }` only — no email,
|
|
14
|
+
* name, IP, or other identifying fields. The server schema enforces
|
|
15
|
+
* the same shape; extras would be rejected with `validationFailed`.
|
|
16
|
+
*/
|
|
17
|
+
export function setUser(user: User | null): void {
|
|
18
|
+
_user = user
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getUser(): User | null {
|
|
22
|
+
return _user
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function captureError(error: Error, extras?: CaptureExtras): void {
|
|
26
|
+
if (!isInitialized()) return
|
|
27
|
+
const cfg = getConfig()!
|
|
28
|
+
const event: Event = {
|
|
29
|
+
app: { version: parseRelease(cfg.release).version },
|
|
30
|
+
breadcrumbs: getBreadcrumbs(),
|
|
31
|
+
device: detectDevice(),
|
|
32
|
+
environment: cfg.environment,
|
|
33
|
+
error: errorToObject(error),
|
|
34
|
+
fingerprint: extras?.fingerprint,
|
|
35
|
+
id: uuidV7(),
|
|
36
|
+
kind: 'error',
|
|
37
|
+
platform: 'javascript',
|
|
38
|
+
release: cfg.release,
|
|
39
|
+
tags: extras?.tags,
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
user: extras?.user ?? _user,
|
|
42
|
+
}
|
|
43
|
+
void send({ ingestUrl: cfg.ingestUrl, token: cfg.token }, event)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const captureException = captureError
|
|
47
|
+
|
|
48
|
+
function errorToObject(error: Error): SentoriError {
|
|
49
|
+
const causeRaw = (error as { cause?: unknown }).cause
|
|
50
|
+
let cause: SentoriError | null = null
|
|
51
|
+
if (causeRaw instanceof Error) cause = errorToObject(causeRaw)
|
|
52
|
+
return {
|
|
53
|
+
cause,
|
|
54
|
+
message: error.message,
|
|
55
|
+
stack: parseStack(error.stack),
|
|
56
|
+
type: error.name || 'Error',
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseRelease(release: string): { build?: string; version: string } {
|
|
61
|
+
const m = /^(?:[^@]+@)?([^+]+)(?:\+(.+))?$/.exec(release)
|
|
62
|
+
return { build: m?.[2], version: m?.[1] ?? '0.0.0' }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function detectDevice(): Event['device'] {
|
|
66
|
+
// Browser: light-touch UA sniff. We deliberately avoid full
|
|
67
|
+
// fingerprinting — the field is for grouping context, not analytics.
|
|
68
|
+
const w = (globalThis as { navigator?: { language?: string; userAgent?: string } }).navigator
|
|
69
|
+
if (w?.userAgent) {
|
|
70
|
+
return {
|
|
71
|
+
locale: w.language,
|
|
72
|
+
os: detectBrowserOs(w.userAgent),
|
|
73
|
+
osVersion: '0',
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Node
|
|
77
|
+
const p = (globalThis as { process?: { platform?: string; version?: string } }).process
|
|
78
|
+
if (p?.platform) {
|
|
79
|
+
return {
|
|
80
|
+
os: p.platform,
|
|
81
|
+
osVersion: p.version?.replace(/^v/, '') ?? '0',
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { os: 'unknown', osVersion: '0' }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function detectBrowserOs(ua: string): string {
|
|
88
|
+
if (ua.includes('Mac OS X') || ua.includes('Macintosh')) return 'macos'
|
|
89
|
+
if (ua.includes('Windows')) return 'windows'
|
|
90
|
+
if (ua.includes('Linux')) return 'linux'
|
|
91
|
+
if (ua.includes('Android')) return 'android'
|
|
92
|
+
if (ua.includes('iPhone') || ua.includes('iPad')) return 'ios'
|
|
93
|
+
return 'web'
|
|
94
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { InitOptions } from './types.js'
|
|
2
|
+
|
|
3
|
+
let _cfg: InitOptions | null = null
|
|
4
|
+
|
|
5
|
+
export function setConfig(cfg: InitOptions): void {
|
|
6
|
+
_cfg = cfg
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getConfig(): InitOptions | null {
|
|
10
|
+
return _cfg
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isInitialized(): boolean {
|
|
14
|
+
return _cfg !== null
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { captureError } from '../capture.js'
|
|
2
|
+
|
|
3
|
+
let installed = false
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wire window.onerror + unhandledrejection so uncaught browser errors
|
|
7
|
+
* land as Sentori events automatically. Idempotent — safe to call
|
|
8
|
+
* twice; the second call no-ops.
|
|
9
|
+
*/
|
|
10
|
+
export function installBrowserHooks(): boolean {
|
|
11
|
+
if (installed) return true
|
|
12
|
+
const w = globalThis as {
|
|
13
|
+
addEventListener?: (
|
|
14
|
+
type: string,
|
|
15
|
+
handler: (e: Event | PromiseRejectionEvent | ErrorEvent) => void
|
|
16
|
+
) => void
|
|
17
|
+
}
|
|
18
|
+
if (typeof w.addEventListener !== 'function') return false
|
|
19
|
+
|
|
20
|
+
const onError = (e: Event | ErrorEvent) => {
|
|
21
|
+
const err = (e as ErrorEvent).error
|
|
22
|
+
if (err instanceof Error) captureError(err)
|
|
23
|
+
else if (typeof (e as ErrorEvent).message === 'string') {
|
|
24
|
+
captureError(new Error((e as ErrorEvent).message))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const onRejection = (e: Event | PromiseRejectionEvent) => {
|
|
29
|
+
const reason = (e as PromiseRejectionEvent).reason
|
|
30
|
+
if (reason instanceof Error) captureError(reason)
|
|
31
|
+
else captureError(new Error(typeof reason === 'string' ? reason : 'unhandled rejection'))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
w.addEventListener('error', onError)
|
|
35
|
+
w.addEventListener('unhandledrejection', onRejection)
|
|
36
|
+
installed = true
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Test helper — resets the idempotency latch. */
|
|
41
|
+
export function _resetBrowserHooksForTesting(): void {
|
|
42
|
+
installed = false
|
|
43
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { captureError } from '../capture.js'
|
|
2
|
+
|
|
3
|
+
let installed = false
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wire process.on('uncaughtException') + 'unhandledRejection'.
|
|
7
|
+
* Idempotent. Returns false if not running on Node (no `process.on`).
|
|
8
|
+
*
|
|
9
|
+
* Node policy notes:
|
|
10
|
+
* - We do NOT call process.exit on uncaughtException; Sentori doesn't
|
|
11
|
+
* own the host's crash strategy. The host's existing handler
|
|
12
|
+
* (default: log + exit 1) runs after ours.
|
|
13
|
+
* - Bun + Deno expose process.on for compatibility; the same code
|
|
14
|
+
* path covers them.
|
|
15
|
+
*/
|
|
16
|
+
export function installNodeHooks(): boolean {
|
|
17
|
+
if (installed) return true
|
|
18
|
+
const p = (globalThis as { process?: NodeJS.Process }).process
|
|
19
|
+
if (!p || typeof p.on !== 'function') return false
|
|
20
|
+
|
|
21
|
+
p.on('uncaughtException', (err: Error) => {
|
|
22
|
+
captureError(err)
|
|
23
|
+
})
|
|
24
|
+
p.on('unhandledRejection', (reason: unknown) => {
|
|
25
|
+
if (reason instanceof Error) captureError(reason)
|
|
26
|
+
else captureError(new Error(typeof reason === 'string' ? reason : 'unhandled rejection'))
|
|
27
|
+
})
|
|
28
|
+
installed = true
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function _resetNodeHooksForTesting(): void {
|
|
33
|
+
installed = false
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { addBreadcrumb, clearBreadcrumbs, getBreadcrumbs } from './breadcrumbs.js'
|
|
2
|
+
export { captureError, captureException, getUser, setUser } from './capture.js'
|
|
3
|
+
export { initSentori } from './init.js'
|
|
4
|
+
export type {
|
|
5
|
+
Breadcrumb,
|
|
6
|
+
BreadcrumbType,
|
|
7
|
+
CaptureExtras,
|
|
8
|
+
Event,
|
|
9
|
+
Frame,
|
|
10
|
+
InitOptions,
|
|
11
|
+
SentoriError,
|
|
12
|
+
Tags,
|
|
13
|
+
User,
|
|
14
|
+
} from './types.js'
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { setConfig } from './config.js'
|
|
2
|
+
import { installBrowserHooks } from './hooks/browser.js'
|
|
3
|
+
import { installNodeHooks } from './hooks/node.js'
|
|
4
|
+
import type { InitOptions } from './types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configure the SDK and (by default) wire global error handlers.
|
|
8
|
+
*
|
|
9
|
+
* Browser: window 'error' + 'unhandledrejection' → captureError.
|
|
10
|
+
* Node: process 'uncaughtException' + 'unhandledRejection' → captureError.
|
|
11
|
+
*
|
|
12
|
+
* Pass `enableGlobalHooks: false` if you want to drive captures
|
|
13
|
+
* manually (e.g. tests, or a host that owns its own crash plumbing).
|
|
14
|
+
*/
|
|
15
|
+
export function initSentori(options: InitOptions): void {
|
|
16
|
+
setConfig(options)
|
|
17
|
+
if (options.enableGlobalHooks === false) return
|
|
18
|
+
// Browser comes first because both globals can exist in some
|
|
19
|
+
// bundlers' shims; we want browser semantics on the web.
|
|
20
|
+
if (!installBrowserHooks()) installNodeHooks()
|
|
21
|
+
}
|
package/src/stack.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Frame } from './types.js'
|
|
2
|
+
|
|
3
|
+
// V8 / Node / Bun: "at fn (file:line:col)" or "at file:line:col" or "at fn file:line:col"
|
|
4
|
+
// File can be a URL (https://…), so we anchor on the trailing `:\d+:\d+\)?$`
|
|
5
|
+
// and let `(?<file>.+?)` swallow whatever comes before.
|
|
6
|
+
const V8_RE = /^\s*at\s+(?:(?<fn>.+?)\s+)?\(?(?<file>.+?):(?<line>\d+):(?<col>\d+)\)?\s*$/
|
|
7
|
+
// SpiderMonkey / Safari: "fn@file:line:col" — same trailing anchor.
|
|
8
|
+
const SPIDER_RE = /^(?:(?<fn>[^@]*)@)?(?<file>.+?):(?<line>\d+):(?<col>\d+)\s*$/
|
|
9
|
+
|
|
10
|
+
/** Best-effort parse of an `Error.stack` string into Sentori frames. */
|
|
11
|
+
export function parseStack(stack: string | undefined): Frame[] {
|
|
12
|
+
if (!stack) return []
|
|
13
|
+
const lines = stack.split('\n')
|
|
14
|
+
const out: Frame[] = []
|
|
15
|
+
for (const raw of lines) {
|
|
16
|
+
const line = raw.trim()
|
|
17
|
+
if (!line) continue
|
|
18
|
+
const m = V8_RE.exec(line) ?? SPIDER_RE.exec(line)
|
|
19
|
+
if (!m?.groups) continue
|
|
20
|
+
const file = m.groups.file ?? '<anonymous>'
|
|
21
|
+
out.push({
|
|
22
|
+
absolutePath: file,
|
|
23
|
+
column: Number(m.groups.col),
|
|
24
|
+
file: shortFile(file),
|
|
25
|
+
function: m.groups.fn?.trim(),
|
|
26
|
+
inApp: !file.includes('node_modules') && !file.startsWith('node:'),
|
|
27
|
+
line: Number(m.groups.line),
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
return out
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shortFile(absolute: string): string {
|
|
34
|
+
// Strip protocol + leading path noise so the dashboard shows
|
|
35
|
+
// e.g. "App.tsx" instead of "https://example.com/static/App.tsx".
|
|
36
|
+
const noProto = absolute.replace(/^https?:\/\/[^/]+\//, '')
|
|
37
|
+
const tail = noProto.split('/').slice(-2).join('/')
|
|
38
|
+
return tail || absolute
|
|
39
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Event } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal HTTP transport. POST /v1/events with a Bearer token.
|
|
5
|
+
* - Browser: prefers `navigator.sendBeacon` on page-unload paths;
|
|
6
|
+
* otherwise plain fetch with `keepalive: true` so events survive
|
|
7
|
+
* a tab close mid-flight.
|
|
8
|
+
* - Node: plain fetch (Node 18+ has it global).
|
|
9
|
+
*
|
|
10
|
+
* On 4xx/5xx the SDK currently drops the event silently — retry +
|
|
11
|
+
* persistent queue is a follow-up if anyone actually wants it.
|
|
12
|
+
*/
|
|
13
|
+
export type TransportConfig = {
|
|
14
|
+
ingestUrl: string
|
|
15
|
+
token: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function send(cfg: TransportConfig, event: Event): Promise<void> {
|
|
19
|
+
const url = `${cfg.ingestUrl.replace(/\/+$/, '')}/v1/events`
|
|
20
|
+
const body = JSON.stringify(event)
|
|
21
|
+
const headers = {
|
|
22
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'Sentori-Sdk': 'sentori-javascript/0.1.0',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Browser: navigator.sendBeacon is fire-and-forget and survives
|
|
28
|
+
// tab close. Bound by user-agent quotas (~64KB), so we feature-detect
|
|
29
|
+
// and only use it for small bodies.
|
|
30
|
+
const beacon = (globalThis as { navigator?: { sendBeacon?: (u: string, b: Blob) => boolean } })
|
|
31
|
+
.navigator?.sendBeacon
|
|
32
|
+
if (typeof beacon === 'function' && body.length < 60_000) {
|
|
33
|
+
try {
|
|
34
|
+
const blob = new Blob([body], { type: 'application/json' })
|
|
35
|
+
// sendBeacon doesn't carry headers — Authorization moves into
|
|
36
|
+
// a query param so the server's existing Bearer auth still works.
|
|
37
|
+
const beaconUrl = `${url}?token=${encodeURIComponent(cfg.token)}`
|
|
38
|
+
if (beacon.call(globalThis.navigator, beaconUrl, blob)) return
|
|
39
|
+
} catch {
|
|
40
|
+
// fall through to fetch
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await fetch(url, {
|
|
46
|
+
body,
|
|
47
|
+
headers,
|
|
48
|
+
keepalive: true,
|
|
49
|
+
method: 'POST',
|
|
50
|
+
})
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// No retry — log and forget. Hosts that care can wrap and add
|
|
53
|
+
// their own retry policy at the app layer.
|
|
54
|
+
if (typeof console !== 'undefined') {
|
|
55
|
+
console.warn('[sentori] transport failed:', (e as Error).message)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire shape for an event sent to the Sentori `/v1/events` endpoint.
|
|
3
|
+
* Identical to the protocol documented in `docs/protocol.md` and the
|
|
4
|
+
* server's `event::Event` Rust type.
|
|
5
|
+
*/
|
|
6
|
+
export type Event = {
|
|
7
|
+
app: { build?: string; framework?: { name: string; version: string }; version: string }
|
|
8
|
+
breadcrumbs: Breadcrumb[]
|
|
9
|
+
device: { locale?: string; model?: string; os: string; osVersion: string }
|
|
10
|
+
environment: string
|
|
11
|
+
error: SentoriError
|
|
12
|
+
fingerprint?: string[]
|
|
13
|
+
id: string
|
|
14
|
+
kind: 'error'
|
|
15
|
+
platform: 'javascript'
|
|
16
|
+
release: string
|
|
17
|
+
spanId?: null | string
|
|
18
|
+
tags?: Tags
|
|
19
|
+
timestamp: string
|
|
20
|
+
traceId?: null | string
|
|
21
|
+
user?: null | User
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SentoriError = {
|
|
25
|
+
cause: null | SentoriError
|
|
26
|
+
message: string
|
|
27
|
+
stack: Frame[]
|
|
28
|
+
type: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type Frame = {
|
|
32
|
+
absolutePath?: string
|
|
33
|
+
column?: number
|
|
34
|
+
file: string
|
|
35
|
+
function?: string
|
|
36
|
+
inApp: boolean
|
|
37
|
+
line: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type BreadcrumbType = 'custom' | 'log' | 'nav' | 'net' | 'user'
|
|
41
|
+
|
|
42
|
+
export type Breadcrumb = {
|
|
43
|
+
data: Record<string, unknown>
|
|
44
|
+
timestamp: string
|
|
45
|
+
type: BreadcrumbType
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** PII-minimal — same shape as the RN SDK and the server schema. */
|
|
49
|
+
export type User = { anonymous?: boolean; id?: string }
|
|
50
|
+
|
|
51
|
+
export type Tags = Record<string, string>
|
|
52
|
+
|
|
53
|
+
export type CaptureExtras = {
|
|
54
|
+
fingerprint?: string[]
|
|
55
|
+
tags?: Tags
|
|
56
|
+
user?: User
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type InitOptions = {
|
|
60
|
+
/** Override automatic global hooks. Default: true on browser + node. */
|
|
61
|
+
enableGlobalHooks?: boolean
|
|
62
|
+
/** "prod" / "dev" / "staging" / whatever you want. */
|
|
63
|
+
environment: string
|
|
64
|
+
/** e.g. https://ingest.sentori.golia.jp */
|
|
65
|
+
ingestUrl: string
|
|
66
|
+
/** e.g. "myapp@1.2.3+456" */
|
|
67
|
+
release: string
|
|
68
|
+
/** st_pk_<26 base32 chars> */
|
|
69
|
+
token: string
|
|
70
|
+
}
|
package/src/uuid.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uuid v7 (timestamp-prefixed). Modern Node ≥ 19 + browsers expose
|
|
3
|
+
* `crypto.randomUUID()` for v4 — that gives us the entropy half;
|
|
4
|
+
* v7 layout is cheap to assemble manually.
|
|
5
|
+
*
|
|
6
|
+
* Layout (RFC 9562 v7):
|
|
7
|
+
* ms (48 bits) | ver=7 (4) | rand_a (12) | var=10 (2) | rand_b (62)
|
|
8
|
+
*/
|
|
9
|
+
export function uuidV7(): string {
|
|
10
|
+
const ms = Date.now()
|
|
11
|
+
const rand = new Uint8Array(10)
|
|
12
|
+
cryptoRandomFill(rand)
|
|
13
|
+
|
|
14
|
+
// 6 bytes of timestamp (ms), big-endian
|
|
15
|
+
const t = new Uint8Array(6)
|
|
16
|
+
let n = ms
|
|
17
|
+
for (let i = 5; i >= 0; i--) {
|
|
18
|
+
t[i] = n & 0xff
|
|
19
|
+
n = Math.floor(n / 256)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// pack version + variant
|
|
23
|
+
rand[0] = (rand[0]! & 0x0f) | 0x70 // version 7 in high nibble of byte 6
|
|
24
|
+
rand[2] = (rand[2]! & 0x3f) | 0x80 // variant 10 in high two bits of byte 8
|
|
25
|
+
|
|
26
|
+
const bytes = new Uint8Array(16)
|
|
27
|
+
bytes.set(t, 0)
|
|
28
|
+
bytes.set(rand, 6)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
hex(bytes.subarray(0, 4)) +
|
|
32
|
+
'-' +
|
|
33
|
+
hex(bytes.subarray(4, 6)) +
|
|
34
|
+
'-' +
|
|
35
|
+
hex(bytes.subarray(6, 8)) +
|
|
36
|
+
'-' +
|
|
37
|
+
hex(bytes.subarray(8, 10)) +
|
|
38
|
+
'-' +
|
|
39
|
+
hex(bytes.subarray(10, 16))
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cryptoRandomFill(buf: Uint8Array): void {
|
|
44
|
+
// Browser + Node 19+ + Bun all expose globalThis.crypto.
|
|
45
|
+
const c = (globalThis as { crypto?: { getRandomValues?: (b: Uint8Array) => void } }).crypto
|
|
46
|
+
if (c?.getRandomValues) {
|
|
47
|
+
c.getRandomValues(buf)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
// Last-resort Math.random (only hits in very old envs; entropy
|
|
51
|
+
// quality is bad but we still want a unique-ish id).
|
|
52
|
+
for (let i = 0; i < buf.length; i++) buf[i] = Math.floor(Math.random() * 256)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hex(b: Uint8Array): string {
|
|
56
|
+
let s = ''
|
|
57
|
+
for (const x of b) s += x.toString(16).padStart(2, '0')
|
|
58
|
+
return s
|
|
59
|
+
}
|