@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.
Files changed (56) hide show
  1. package/lib/breadcrumbs.d.ts +9 -0
  2. package/lib/breadcrumbs.d.ts.map +1 -0
  3. package/lib/breadcrumbs.js +19 -0
  4. package/lib/breadcrumbs.js.map +1 -0
  5. package/lib/capture.d.ts +13 -0
  6. package/lib/capture.d.ts.map +1 -0
  7. package/lib/capture.js +92 -0
  8. package/lib/capture.js.map +1 -0
  9. package/lib/config.d.ts +5 -0
  10. package/lib/config.d.ts.map +1 -0
  11. package/lib/config.js +11 -0
  12. package/lib/config.js.map +1 -0
  13. package/lib/hooks/browser.d.ts +9 -0
  14. package/lib/hooks/browser.d.ts.map +1 -0
  15. package/lib/hooks/browser.js +38 -0
  16. package/lib/hooks/browser.js.map +1 -0
  17. package/lib/hooks/node.d.ts +14 -0
  18. package/lib/hooks/node.d.ts.map +1 -0
  19. package/lib/hooks/node.js +35 -0
  20. package/lib/hooks/node.js.map +1 -0
  21. package/lib/index.d.ts +5 -0
  22. package/lib/index.d.ts.map +1 -0
  23. package/lib/index.js +4 -0
  24. package/lib/index.js.map +1 -0
  25. package/lib/init.d.ts +12 -0
  26. package/lib/init.d.ts.map +1 -0
  27. package/lib/init.js +22 -0
  28. package/lib/init.js.map +1 -0
  29. package/lib/stack.d.ts +4 -0
  30. package/lib/stack.d.ts.map +1 -0
  31. package/lib/stack.js +39 -0
  32. package/lib/stack.js.map +1 -0
  33. package/lib/transport.d.ts +17 -0
  34. package/lib/transport.d.ts.map +1 -0
  35. package/lib/transport.js +43 -0
  36. package/lib/transport.js.map +1 -0
  37. package/lib/types.d.ts +78 -0
  38. package/lib/types.d.ts.map +1 -0
  39. package/lib/types.js +2 -0
  40. package/lib/types.js.map +1 -0
  41. package/lib/uuid.d.ts +10 -0
  42. package/lib/uuid.d.ts.map +1 -0
  43. package/lib/uuid.js +54 -0
  44. package/lib/uuid.js.map +1 -0
  45. package/package.json +54 -0
  46. package/src/breadcrumbs.ts +27 -0
  47. package/src/capture.ts +94 -0
  48. package/src/config.ts +15 -0
  49. package/src/hooks/browser.ts +43 -0
  50. package/src/hooks/node.ts +34 -0
  51. package/src/index.ts +14 -0
  52. package/src/init.ts +21 -0
  53. package/src/stack.ts +39 -0
  54. package/src/transport.ts +58 -0
  55. package/src/types.ts +70 -0
  56. 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"}
@@ -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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"}
@@ -0,0 +1,5 @@
1
+ import type { InitOptions } from './types.js';
2
+ export declare function setConfig(cfg: InitOptions): void;
3
+ export declare function getConfig(): InitOptions | null;
4
+ export declare function isInitialized(): boolean;
5
+ //# sourceMappingURL=config.d.ts.map
@@ -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,11 @@
1
+ let _cfg = null;
2
+ export function setConfig(cfg) {
3
+ _cfg = cfg;
4
+ }
5
+ export function getConfig() {
6
+ return _cfg;
7
+ }
8
+ export function isInitialized() {
9
+ return _cfg !== null;
10
+ }
11
+ //# sourceMappingURL=config.js.map
@@ -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
@@ -0,0 +1,4 @@
1
+ export { addBreadcrumb, clearBreadcrumbs, getBreadcrumbs } from './breadcrumbs.js';
2
+ export { captureError, captureException, getUser, setUser } from './capture.js';
3
+ export { initSentori } from './init.js';
4
+ //# sourceMappingURL=index.js.map
@@ -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
@@ -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,4 @@
1
+ import type { Frame } from './types.js';
2
+ /** Best-effort parse of an `Error.stack` string into Sentori frames. */
3
+ export declare function parseStack(stack: string | undefined): Frame[];
4
+ //# sourceMappingURL=stack.d.ts.map
@@ -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
@@ -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"}
@@ -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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
@@ -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
+ }
@@ -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
+ }