@goliapkg/sentori-react 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/SentoriErrorBoundary.d.ts +20 -0
- package/lib/SentoriErrorBoundary.d.ts.map +1 -0
- package/lib/SentoriErrorBoundary.js +32 -0
- package/lib/SentoriErrorBoundary.js.map +1 -0
- package/lib/SentoriProvider.d.ts +21 -0
- package/lib/SentoriProvider.d.ts.map +1 -0
- package/lib/SentoriProvider.js +71 -0
- package/lib/SentoriProvider.js.map +1 -0
- package/lib/__tests__/setup.d.ts +2 -0
- package/lib/__tests__/setup.d.ts.map +1 -0
- package/lib/__tests__/setup.js +12 -0
- package/lib/__tests__/setup.js.map +1 -0
- package/lib/hooks.d.ts +28 -0
- package/lib/hooks.d.ts.map +1 -0
- package/lib/hooks.js +42 -0
- package/lib/hooks.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/types.d.ts +29 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +60 -0
- package/src/SentoriErrorBoundary.tsx +56 -0
- package/src/SentoriProvider.tsx +96 -0
- package/src/__tests__/SentoriErrorBoundary.test.tsx +51 -0
- package/src/__tests__/hooks.test.tsx +48 -0
- package/src/__tests__/setup.ts +15 -0
- package/src/hooks.ts +51 -0
- package/src/index.ts +13 -0
- package/src/types.ts +40 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
type Props = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
/** Rendered after an error is caught. Receives the error and a
|
|
5
|
+
* `reset` callback that clears the boundary so retries can run. */
|
|
6
|
+
fallback: (props: {
|
|
7
|
+
error: Error;
|
|
8
|
+
reset: () => void;
|
|
9
|
+
}) => ReactNode;
|
|
10
|
+
/** Optional additional logging hook. Runs after Sentori capture. */
|
|
11
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Wraps `<SentoriErrorBoundaryInner>` so we can grab the capture
|
|
15
|
+
* function from context — class components can't use hooks directly,
|
|
16
|
+
* but they CAN receive props from a thin functional wrapper.
|
|
17
|
+
*/
|
|
18
|
+
export declare function SentoriErrorBoundary(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=SentoriErrorBoundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SentoriErrorBoundary.d.ts","sourceRoot":"","sources":["../src/SentoriErrorBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAIjE,KAAK,KAAK,GAAG;IACX,QAAQ,EAAE,SAAS,CAAA;IACnB;wEACoE;IACpE,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAA;IACnE,oEAAoE;IACpE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;CAClD,CAAA;AAID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,2CAWhD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
import { useSentoriCtx } from './SentoriProvider.js';
|
|
4
|
+
/**
|
|
5
|
+
* Wraps `<SentoriErrorBoundaryInner>` so we can grab the capture
|
|
6
|
+
* function from context — class components can't use hooks directly,
|
|
7
|
+
* but they CAN receive props from a thin functional wrapper.
|
|
8
|
+
*/
|
|
9
|
+
export function SentoriErrorBoundary(props) {
|
|
10
|
+
const { captureError } = useSentoriCtx();
|
|
11
|
+
return (_jsx(SentoriErrorBoundaryInner, { ...props, capture: (err, info) => {
|
|
12
|
+
captureError(err, { tags: { source: 'react.errorBoundary' } });
|
|
13
|
+
props.onError?.(err, info);
|
|
14
|
+
} }));
|
|
15
|
+
}
|
|
16
|
+
class SentoriErrorBoundaryInner extends Component {
|
|
17
|
+
state = { error: null };
|
|
18
|
+
static getDerivedStateFromError(error) {
|
|
19
|
+
return { error };
|
|
20
|
+
}
|
|
21
|
+
componentDidCatch(error, info) {
|
|
22
|
+
this.props.capture(error, info);
|
|
23
|
+
}
|
|
24
|
+
reset = () => this.setState({ error: null });
|
|
25
|
+
render() {
|
|
26
|
+
if (this.state.error) {
|
|
27
|
+
return this.props.fallback({ error: this.state.error, reset: this.reset });
|
|
28
|
+
}
|
|
29
|
+
return this.props.children;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=SentoriErrorBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SentoriErrorBoundary.js","sourceRoot":"","sources":["../src/SentoriErrorBoundary.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAkC,MAAM,OAAO,CAAA;AAEjE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAapD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAY;IAC/C,MAAM,EAAE,YAAY,EAAE,GAAG,aAAa,EAAE,CAAA;IACxC,OAAO,CACL,KAAC,yBAAyB,OACpB,KAAK,EACT,OAAO,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;YACrB,YAAY,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,qBAAqB,EAAE,EAAE,CAAC,CAAA;YAC9D,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QAC5B,CAAC,GACD,CACH,CAAA;AACH,CAAC;AAED,MAAM,yBAA0B,SAAQ,SAGvC;IACC,KAAK,GAAU,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAE9B,MAAM,CAAC,wBAAwB,CAAC,KAAY;QAC1C,OAAO,EAAE,KAAK,EAAE,CAAA;IAClB,CAAC;IAED,iBAAiB,CAAC,KAAY,EAAE,IAAe;QAC7C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACjC,CAAC;IAED,KAAK,GAAG,GAAS,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAElD,MAAM;QACJ,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;QAC5E,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAA;IAC5B,CAAC;CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { SentoriContextValue, SentoriReactConfig } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Initialises the JS SDK once on mount and exposes capture / breadcrumb
|
|
5
|
+
* helpers via context. Safe to mount multiple times in dev (StrictMode
|
|
6
|
+
* double-mount): the JS SDK's own idempotency guards take care of it
|
|
7
|
+
* but we also dedupe here via a ref.
|
|
8
|
+
*
|
|
9
|
+
* Drop this near the root of the React tree (above any
|
|
10
|
+
* `<SentoriErrorBoundary>`):
|
|
11
|
+
*
|
|
12
|
+
* <SentoriProvider config={{ token, release, ingestUrl, environment }}>
|
|
13
|
+
* <App />
|
|
14
|
+
* </SentoriProvider>
|
|
15
|
+
*/
|
|
16
|
+
export declare function SentoriProvider({ children, config, }: {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
config: SentoriReactConfig;
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export declare function useSentoriCtx(): SentoriContextValue;
|
|
21
|
+
//# sourceMappingURL=SentoriProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SentoriProvider.d.ts","sourceRoot":"","sources":["../src/SentoriProvider.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAiB,KAAK,SAAS,EAAyC,MAAM,OAAO,CAAA;AAE5F,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAQ,MAAM,YAAY,CAAA;AAM/E;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,SAAS,CAAA;IACnB,MAAM,EAAE,kBAAkB,CAAA;CAC3B,2CAuCA;AAaD,wBAAgB,aAAa,IAAI,mBAAmB,CASnD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { addBreadcrumb, captureError, captureException, initSentori, setUser, } from '@goliapkg/sentori-javascript';
|
|
3
|
+
import { createContext, useContext, useMemo, useRef, useState } from 'react';
|
|
4
|
+
const Ctx = createContext(null);
|
|
5
|
+
let _tags = null;
|
|
6
|
+
/**
|
|
7
|
+
* Initialises the JS SDK once on mount and exposes capture / breadcrumb
|
|
8
|
+
* helpers via context. Safe to mount multiple times in dev (StrictMode
|
|
9
|
+
* double-mount): the JS SDK's own idempotency guards take care of it
|
|
10
|
+
* but we also dedupe here via a ref.
|
|
11
|
+
*
|
|
12
|
+
* Drop this near the root of the React tree (above any
|
|
13
|
+
* `<SentoriErrorBoundary>`):
|
|
14
|
+
*
|
|
15
|
+
* <SentoriProvider config={{ token, release, ingestUrl, environment }}>
|
|
16
|
+
* <App />
|
|
17
|
+
* </SentoriProvider>
|
|
18
|
+
*/
|
|
19
|
+
export function SentoriProvider({ children, config, }) {
|
|
20
|
+
const initialisedRef = useRef(false);
|
|
21
|
+
const [initialised, setInitialised] = useState(false);
|
|
22
|
+
if (!initialisedRef.current) {
|
|
23
|
+
initialisedRef.current = true;
|
|
24
|
+
try {
|
|
25
|
+
initSentori(config);
|
|
26
|
+
setInitialised(true);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
// Misconfiguration (bad token shape, missing fields). Surface to
|
|
30
|
+
// console but don't crash the app — the rest of the tree should
|
|
31
|
+
// still render.
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.error('[sentori-react] init failed', e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const value = useMemo(() => ({
|
|
37
|
+
addBreadcrumb: (type, data) => {
|
|
38
|
+
addBreadcrumb({ data: data ?? {}, type });
|
|
39
|
+
},
|
|
40
|
+
captureError: (err, extras) => {
|
|
41
|
+
captureError(err, mergeExtras(extras));
|
|
42
|
+
},
|
|
43
|
+
captureException: (err, extras) => {
|
|
44
|
+
captureException(err, mergeExtras(extras));
|
|
45
|
+
},
|
|
46
|
+
initialised,
|
|
47
|
+
setTags: (tags) => {
|
|
48
|
+
_tags = tags;
|
|
49
|
+
},
|
|
50
|
+
setUser,
|
|
51
|
+
}), [initialised]);
|
|
52
|
+
return _jsx(Ctx.Provider, { value: value, children: children });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Merge provider-scoped tags into per-call extras. Per-call wins over
|
|
56
|
+
* provider-scoped on conflict (matches Sentry's semantics).
|
|
57
|
+
*/
|
|
58
|
+
function mergeExtras(extras) {
|
|
59
|
+
if (!_tags)
|
|
60
|
+
return extras;
|
|
61
|
+
return { ...extras, tags: { ..._tags, ...(extras?.tags ?? {}) } };
|
|
62
|
+
}
|
|
63
|
+
export function useSentoriCtx() {
|
|
64
|
+
const ctx = useContext(Ctx);
|
|
65
|
+
if (!ctx) {
|
|
66
|
+
throw new Error('[sentori-react] hook used outside <SentoriProvider>. ' +
|
|
67
|
+
'Wrap your app at or above the component that calls useSentori / useCaptureError.');
|
|
68
|
+
}
|
|
69
|
+
return ctx;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=SentoriProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SentoriProvider.js","sourceRoot":"","sources":["../src/SentoriProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,GACR,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAI5F,MAAM,GAAG,GAAG,aAAa,CAA6B,IAAI,CAAC,CAAA;AAE3D,IAAI,KAAK,GAAgB,IAAI,CAAA;AAE7B;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,QAAQ,EACR,MAAM,GAIP;IACC,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACpC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAErD,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC;QAC5B,cAAc,CAAC,OAAO,GAAG,IAAI,CAAA;QAC7B,IAAI,CAAC;YACH,WAAW,CAAC,MAAM,CAAC,CAAA;YACnB,cAAc,CAAC,IAAI,CAAC,CAAA;QACtB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,iEAAiE;YACjE,gEAAgE;YAChE,gBAAgB;YAChB,sCAAsC;YACtC,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,CAAC,CAAC,CAAA;QACjD,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAwB,OAAO,CACxC,GAAG,EAAE,CAAC,CAAC;QACL,aAAa,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YAC5B,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3C,CAAC;QACD,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YAC5B,YAAY,CAAC,GAAG,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;QACxC,CAAC;QACD,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YAChC,gBAAgB,CAAC,GAAG,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;QAC5C,CAAC;QACD,WAAW;QACX,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YAChB,KAAK,GAAG,IAAI,CAAA;QACd,CAAC;QACD,OAAO;KACR,CAAC,EACF,CAAC,WAAW,CAAC,CACd,CAAA;IAED,OAAO,KAAC,GAAG,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAgB,CAAA;AAC9D,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAClB,MAAwE;IAExE,IAAI,CAAC,KAAK;QAAE,OAAO,MAAM,CAAA;IACzB,OAAO,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAA;AACnE,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;IAC3B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,uDAAuD;YACrD,kFAAkF,CACrF,CAAA;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GlobalRegistrator } from '@happy-dom/global-registrator';
|
|
2
|
+
GlobalRegistrator.register();
|
|
3
|
+
// Phase 21 sub-B: tests run under happy-dom, but the JS SDK's
|
|
4
|
+
// transport tries to POST to ingestUrl. Stub fetch on every layer
|
|
5
|
+
// happy-dom exposes — global, window, and the constructor — so
|
|
6
|
+
// initialisation, hook installs, and capture paths all no-op.
|
|
7
|
+
const stubFetch = async () => new Response('{}', { status: 202 });
|
|
8
|
+
globalThis.fetch = stubFetch;
|
|
9
|
+
window.fetch = stubFetch;
|
|
10
|
+
window.sendBeacon = () => true;
|
|
11
|
+
navigator.sendBeacon = () => true;
|
|
12
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/__tests__/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AAEjE,iBAAiB,CAAC,QAAQ,EAAE,CAAA;AAE5B,8DAA8D;AAC9D,kEAAkE;AAClE,+DAA+D;AAC/D,8DAA8D;AAC9D,MAAM,SAAS,GAAG,KAAK,IAAI,EAAE,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAChE;AAAC,UAAiC,CAAC,KAAK,GAAG,SAAS,CACpD;AAAC,MAAwC,CAAC,KAAK,GAAG,SAAS,CAG3D;AAAC,MAA8C,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC,IAAI,CACvE;AAAC,SAAiD,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC,IAAI,CAAA"}
|
package/lib/hooks.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CaptureExtras, SentoriContextValue } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Imperative access to the SDK from inside a component. Returns the
|
|
4
|
+
* full context — most code only needs `captureError`, `addBreadcrumb`,
|
|
5
|
+
* or `setUser`.
|
|
6
|
+
*
|
|
7
|
+
* const { captureError, addBreadcrumb } = useSentori()
|
|
8
|
+
* try {
|
|
9
|
+
* await api.checkout(order)
|
|
10
|
+
* } catch (e) {
|
|
11
|
+
* captureError(e as Error, { tags: { stage: 'checkout' } })
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
export declare function useSentori(): SentoriContextValue;
|
|
15
|
+
/**
|
|
16
|
+
* Wraps an async function so any thrown / rejected error is captured
|
|
17
|
+
* and rethrown. The wrapper is `useCallback`-stable across renders if
|
|
18
|
+
* `extras` is stable too — pass it inside `useMemo` if you build it
|
|
19
|
+
* inline.
|
|
20
|
+
*
|
|
21
|
+
* const checkout = useCaptureError(
|
|
22
|
+
* async (order: Order) => api.checkout(order),
|
|
23
|
+
* { tags: { stage: 'checkout' } },
|
|
24
|
+
* )
|
|
25
|
+
* await checkout(order) // captures + throws on failure
|
|
26
|
+
*/
|
|
27
|
+
export declare function useCaptureError<TArgs extends unknown[], TRet>(fn: (...args: TArgs) => Promise<TRet> | TRet, extras?: CaptureExtras): (...args: TArgs) => Promise<TRet>;
|
|
28
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAEpE;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,IAAI,mBAAmB,CAEhD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,IAAI,EAC3D,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EAC5C,MAAM,CAAC,EAAE,aAAa,GACrB,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAanC"}
|
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useSentoriCtx } from './SentoriProvider.js';
|
|
3
|
+
/**
|
|
4
|
+
* Imperative access to the SDK from inside a component. Returns the
|
|
5
|
+
* full context — most code only needs `captureError`, `addBreadcrumb`,
|
|
6
|
+
* or `setUser`.
|
|
7
|
+
*
|
|
8
|
+
* const { captureError, addBreadcrumb } = useSentori()
|
|
9
|
+
* try {
|
|
10
|
+
* await api.checkout(order)
|
|
11
|
+
* } catch (e) {
|
|
12
|
+
* captureError(e as Error, { tags: { stage: 'checkout' } })
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export function useSentori() {
|
|
16
|
+
return useSentoriCtx();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wraps an async function so any thrown / rejected error is captured
|
|
20
|
+
* and rethrown. The wrapper is `useCallback`-stable across renders if
|
|
21
|
+
* `extras` is stable too — pass it inside `useMemo` if you build it
|
|
22
|
+
* inline.
|
|
23
|
+
*
|
|
24
|
+
* const checkout = useCaptureError(
|
|
25
|
+
* async (order: Order) => api.checkout(order),
|
|
26
|
+
* { tags: { stage: 'checkout' } },
|
|
27
|
+
* )
|
|
28
|
+
* await checkout(order) // captures + throws on failure
|
|
29
|
+
*/
|
|
30
|
+
export function useCaptureError(fn, extras) {
|
|
31
|
+
const { captureError } = useSentoriCtx();
|
|
32
|
+
return useCallback(async (...args) => {
|
|
33
|
+
try {
|
|
34
|
+
return await fn(...args);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
captureError(e instanceof Error ? e : new Error(String(e)), extras);
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
}, [captureError, fn, extras]);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=hooks.js.map
|
package/lib/hooks.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAIpD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,aAAa,EAAE,CAAA;AACxB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,EAA4C,EAC5C,MAAsB;IAEtB,MAAM,EAAE,YAAY,EAAE,GAAG,aAAa,EAAE,CAAA;IACxC,OAAO,WAAW,CAChB,KAAK,EAAE,GAAG,IAAW,EAAE,EAAE;QACvB,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,CAAA;QAC1B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,YAAY,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;YACnE,MAAM,CAAC,CAAA;QACT,CAAC;IACH,CAAC,EACD,CAAC,YAAY,EAAE,EAAE,EAAE,MAAM,CAAC,CAC3B,CAAA;AACH,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { SentoriProvider } from './SentoriProvider.js';
|
|
2
|
+
export { SentoriErrorBoundary } from './SentoriErrorBoundary.js';
|
|
3
|
+
export { useSentori, useCaptureError } from './hooks.js';
|
|
4
|
+
export type { Breadcrumb, BreadcrumbType, CaptureExtras, SentoriContextValue, SentoriReactConfig, 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,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAExD,YAAY,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,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,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA"}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Breadcrumb, BreadcrumbType, CaptureExtras, CommonInitOptions, Tags, User } from '@goliapkg/sentori-core';
|
|
2
|
+
export type SentoriReactConfig = CommonInitOptions & {
|
|
3
|
+
/**
|
|
4
|
+
* Disable the JS SDK's automatic window/process error hooks. Default
|
|
5
|
+
* is to leave them on so non-React errors (network handlers, top-
|
|
6
|
+
* level promises) still capture. Set to false if you want
|
|
7
|
+
* SentoriErrorBoundary to be the only entry point.
|
|
8
|
+
*/
|
|
9
|
+
enableGlobalHooks?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type SentoriContextValue = {
|
|
12
|
+
/** Append a breadcrumb to the per-process ring buffer. */
|
|
13
|
+
addBreadcrumb: (type: BreadcrumbType, data?: Record<string, unknown>) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Capture any thrown value. Plain `Error` is the happy path; non-
|
|
16
|
+
* Error values get wrapped so the dashboard still sees a stack.
|
|
17
|
+
*/
|
|
18
|
+
captureError: (error: Error, extras?: CaptureExtras) => void;
|
|
19
|
+
/** Same as captureError. Kept for parity with the JS SDK. */
|
|
20
|
+
captureException: (error: Error, extras?: CaptureExtras) => void;
|
|
21
|
+
/** Whether SentoriProvider has finished its one-shot init. */
|
|
22
|
+
initialised: boolean;
|
|
23
|
+
/** Attach a stable user identifier to subsequent events. */
|
|
24
|
+
setUser: (user: null | User) => void;
|
|
25
|
+
/** Replace the entire tag set on subsequent events. */
|
|
26
|
+
setTags: (tags: Tags | null) => void;
|
|
27
|
+
};
|
|
28
|
+
export type { Breadcrumb, BreadcrumbType, CaptureExtras, Tags, User };
|
|
29
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,IAAI,EACJ,IAAI,EACL,MAAM,wBAAwB,CAAA;AAE/B,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG;IACnD;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,0DAA0D;IAC1D,aAAa,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAC7E;;;OAGG;IACH,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5D,6DAA6D;IAC7D,gBAAgB,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;IAChE,8DAA8D;IAC9D,WAAW,EAAE,OAAO,CAAA;IACpB,4DAA4D;IAC5D,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAA;IACpC,uDAAuD;IACvD,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAA;CACrC,CAAA;AAID,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,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/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goliapkg/sentori-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React adapter for Sentori — Provider, ErrorBoundary, and hooks built on @goliapkg/sentori-javascript.",
|
|
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/react"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/goliajp/sentori/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"sentori",
|
|
17
|
+
"error-tracking",
|
|
18
|
+
"react",
|
|
19
|
+
"error-boundary"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./lib/index.js",
|
|
23
|
+
"types": "./lib/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./lib/index.d.ts",
|
|
27
|
+
"default": "./lib/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"lib/",
|
|
32
|
+
"src/",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"prepack": "bun run build"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@goliapkg/sentori-core": "0.1.0",
|
|
46
|
+
"@goliapkg/sentori-javascript": "0.2.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@happy-dom/global-registrator": "^17",
|
|
50
|
+
"@testing-library/react": "^16",
|
|
51
|
+
"@types/bun": "latest",
|
|
52
|
+
"@types/react": "^19",
|
|
53
|
+
"react": "^19",
|
|
54
|
+
"react-dom": "^19",
|
|
55
|
+
"typescript": "^5"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useSentoriCtx } from './SentoriProvider.js'
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
children: ReactNode
|
|
7
|
+
/** Rendered after an error is caught. Receives the error and a
|
|
8
|
+
* `reset` callback that clears the boundary so retries can run. */
|
|
9
|
+
fallback: (props: { error: Error; reset: () => void }) => ReactNode
|
|
10
|
+
/** Optional additional logging hook. Runs after Sentori capture. */
|
|
11
|
+
onError?: (error: Error, info: ErrorInfo) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type State = { error: Error | null }
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wraps `<SentoriErrorBoundaryInner>` so we can grab the capture
|
|
18
|
+
* function from context — class components can't use hooks directly,
|
|
19
|
+
* but they CAN receive props from a thin functional wrapper.
|
|
20
|
+
*/
|
|
21
|
+
export function SentoriErrorBoundary(props: Props) {
|
|
22
|
+
const { captureError } = useSentoriCtx()
|
|
23
|
+
return (
|
|
24
|
+
<SentoriErrorBoundaryInner
|
|
25
|
+
{...props}
|
|
26
|
+
capture={(err, info) => {
|
|
27
|
+
captureError(err, { tags: { source: 'react.errorBoundary' } })
|
|
28
|
+
props.onError?.(err, info)
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class SentoriErrorBoundaryInner extends Component<
|
|
35
|
+
Props & { capture: (e: Error, info: ErrorInfo) => void },
|
|
36
|
+
State
|
|
37
|
+
> {
|
|
38
|
+
state: State = { error: null }
|
|
39
|
+
|
|
40
|
+
static getDerivedStateFromError(error: Error): State {
|
|
41
|
+
return { error }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
45
|
+
this.props.capture(error, info)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
reset = (): void => this.setState({ error: null })
|
|
49
|
+
|
|
50
|
+
render(): ReactNode {
|
|
51
|
+
if (this.state.error) {
|
|
52
|
+
return this.props.fallback({ error: this.state.error, reset: this.reset })
|
|
53
|
+
}
|
|
54
|
+
return this.props.children
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addBreadcrumb,
|
|
3
|
+
captureError,
|
|
4
|
+
captureException,
|
|
5
|
+
initSentori,
|
|
6
|
+
setUser,
|
|
7
|
+
} from '@goliapkg/sentori-javascript'
|
|
8
|
+
import { createContext, type ReactNode, useContext, useMemo, useRef, useState } from 'react'
|
|
9
|
+
|
|
10
|
+
import type { SentoriContextValue, SentoriReactConfig, Tags } from './types.js'
|
|
11
|
+
|
|
12
|
+
const Ctx = createContext<null | SentoriContextValue>(null)
|
|
13
|
+
|
|
14
|
+
let _tags: null | Tags = null
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialises the JS SDK once on mount and exposes capture / breadcrumb
|
|
18
|
+
* helpers via context. Safe to mount multiple times in dev (StrictMode
|
|
19
|
+
* double-mount): the JS SDK's own idempotency guards take care of it
|
|
20
|
+
* but we also dedupe here via a ref.
|
|
21
|
+
*
|
|
22
|
+
* Drop this near the root of the React tree (above any
|
|
23
|
+
* `<SentoriErrorBoundary>`):
|
|
24
|
+
*
|
|
25
|
+
* <SentoriProvider config={{ token, release, ingestUrl, environment }}>
|
|
26
|
+
* <App />
|
|
27
|
+
* </SentoriProvider>
|
|
28
|
+
*/
|
|
29
|
+
export function SentoriProvider({
|
|
30
|
+
children,
|
|
31
|
+
config,
|
|
32
|
+
}: {
|
|
33
|
+
children: ReactNode
|
|
34
|
+
config: SentoriReactConfig
|
|
35
|
+
}) {
|
|
36
|
+
const initialisedRef = useRef(false)
|
|
37
|
+
const [initialised, setInitialised] = useState(false)
|
|
38
|
+
|
|
39
|
+
if (!initialisedRef.current) {
|
|
40
|
+
initialisedRef.current = true
|
|
41
|
+
try {
|
|
42
|
+
initSentori(config)
|
|
43
|
+
setInitialised(true)
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// Misconfiguration (bad token shape, missing fields). Surface to
|
|
46
|
+
// console but don't crash the app — the rest of the tree should
|
|
47
|
+
// still render.
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.error('[sentori-react] init failed', e)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const value: SentoriContextValue = useMemo(
|
|
54
|
+
() => ({
|
|
55
|
+
addBreadcrumb: (type, data) => {
|
|
56
|
+
addBreadcrumb({ data: data ?? {}, type })
|
|
57
|
+
},
|
|
58
|
+
captureError: (err, extras) => {
|
|
59
|
+
captureError(err, mergeExtras(extras))
|
|
60
|
+
},
|
|
61
|
+
captureException: (err, extras) => {
|
|
62
|
+
captureException(err, mergeExtras(extras))
|
|
63
|
+
},
|
|
64
|
+
initialised,
|
|
65
|
+
setTags: (tags) => {
|
|
66
|
+
_tags = tags
|
|
67
|
+
},
|
|
68
|
+
setUser,
|
|
69
|
+
}),
|
|
70
|
+
[initialised],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return <Ctx.Provider value={value}>{children}</Ctx.Provider>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Merge provider-scoped tags into per-call extras. Per-call wins over
|
|
78
|
+
* provider-scoped on conflict (matches Sentry's semantics).
|
|
79
|
+
*/
|
|
80
|
+
function mergeExtras(
|
|
81
|
+
extras?: { fingerprint?: string[]; tags?: Tags; user?: { id?: string } },
|
|
82
|
+
): typeof extras {
|
|
83
|
+
if (!_tags) return extras
|
|
84
|
+
return { ...extras, tags: { ..._tags, ...(extras?.tags ?? {}) } }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function useSentoriCtx(): SentoriContextValue {
|
|
88
|
+
const ctx = useContext(Ctx)
|
|
89
|
+
if (!ctx) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
'[sentori-react] hook used outside <SentoriProvider>. ' +
|
|
92
|
+
'Wrap your app at or above the component that calls useSentori / useCaptureError.',
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return ctx
|
|
96
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, test } from 'bun:test'
|
|
3
|
+
|
|
4
|
+
import { SentoriErrorBoundary } from '../SentoriErrorBoundary.js'
|
|
5
|
+
import { SentoriProvider } from '../SentoriProvider.js'
|
|
6
|
+
|
|
7
|
+
const PROVIDER_PROPS = {
|
|
8
|
+
config: {
|
|
9
|
+
environment: 'test',
|
|
10
|
+
ingestUrl: 'http://localhost:0',
|
|
11
|
+
release: 'test@0.0.0',
|
|
12
|
+
token: 'st_pk_testtesttesttesttesttesttest',
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Boom = (): never => {
|
|
17
|
+
throw new Error('boom-from-render')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('SentoriErrorBoundary', () => {
|
|
21
|
+
test('renders children when nothing throws', () => {
|
|
22
|
+
render(
|
|
23
|
+
<SentoriProvider {...PROVIDER_PROPS}>
|
|
24
|
+
<SentoriErrorBoundary fallback={() => <div>fallback</div>}>
|
|
25
|
+
<div>ok</div>
|
|
26
|
+
</SentoriErrorBoundary>
|
|
27
|
+
</SentoriProvider>,
|
|
28
|
+
)
|
|
29
|
+
expect(screen.getByText('ok')).toBeDefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('renders fallback when child throws', () => {
|
|
33
|
+
// Suppress React's noisy "uncaught error" log in the test output.
|
|
34
|
+
const original = console.error
|
|
35
|
+
console.error = () => {}
|
|
36
|
+
try {
|
|
37
|
+
render(
|
|
38
|
+
<SentoriProvider {...PROVIDER_PROPS}>
|
|
39
|
+
<SentoriErrorBoundary
|
|
40
|
+
fallback={({ error }) => <div>caught: {error.message}</div>}
|
|
41
|
+
>
|
|
42
|
+
<Boom />
|
|
43
|
+
</SentoriErrorBoundary>
|
|
44
|
+
</SentoriProvider>,
|
|
45
|
+
)
|
|
46
|
+
} finally {
|
|
47
|
+
console.error = original
|
|
48
|
+
}
|
|
49
|
+
expect(screen.getByText('caught: boom-from-render')).toBeDefined()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, test } from 'bun:test'
|
|
3
|
+
|
|
4
|
+
import { SentoriProvider } from '../SentoriProvider.js'
|
|
5
|
+
import { useSentori } from '../hooks.js'
|
|
6
|
+
|
|
7
|
+
const PROVIDER_PROPS = {
|
|
8
|
+
config: {
|
|
9
|
+
environment: 'test',
|
|
10
|
+
ingestUrl: 'http://localhost:0',
|
|
11
|
+
release: 'test@0.0.0',
|
|
12
|
+
token: 'st_pk_testtesttesttesttesttesttest',
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('useSentori', () => {
|
|
17
|
+
test('returns capture / setUser / addBreadcrumb', () => {
|
|
18
|
+
const Probe = () => {
|
|
19
|
+
const ctx = useSentori()
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
init={String(ctx.initialised)} hasCapture=
|
|
23
|
+
{String(typeof ctx.captureError === 'function')}
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
render(
|
|
28
|
+
<SentoriProvider {...PROVIDER_PROPS}>
|
|
29
|
+
<Probe />
|
|
30
|
+
</SentoriProvider>,
|
|
31
|
+
)
|
|
32
|
+
expect(screen.getByText(/init=true hasCapture=true/)).toBeDefined()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('throws when used outside provider', () => {
|
|
36
|
+
const Probe = () => {
|
|
37
|
+
useSentori()
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
const original = console.error
|
|
41
|
+
console.error = () => {}
|
|
42
|
+
try {
|
|
43
|
+
expect(() => render(<Probe />)).toThrow(/SentoriProvider/)
|
|
44
|
+
} finally {
|
|
45
|
+
console.error = original
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GlobalRegistrator } from '@happy-dom/global-registrator'
|
|
2
|
+
|
|
3
|
+
GlobalRegistrator.register()
|
|
4
|
+
|
|
5
|
+
// Phase 21 sub-B: tests run under happy-dom, but the JS SDK's
|
|
6
|
+
// transport tries to POST to ingestUrl. Stub fetch on every layer
|
|
7
|
+
// happy-dom exposes — global, window, and the constructor — so
|
|
8
|
+
// initialisation, hook installs, and capture paths all no-op.
|
|
9
|
+
const stubFetch = async () => new Response('{}', { status: 202 })
|
|
10
|
+
;(globalThis as { fetch: unknown }).fetch = stubFetch
|
|
11
|
+
;(window as unknown as { fetch: unknown }).fetch = stubFetch
|
|
12
|
+
// Some happy-dom versions snapshot the original fetch constructor
|
|
13
|
+
// onto window.Window.prototype; overwriting our reference is harmless.
|
|
14
|
+
;(window as unknown as { sendBeacon?: unknown }).sendBeacon = () => true
|
|
15
|
+
;(navigator as unknown as { sendBeacon?: unknown }).sendBeacon = () => true
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useSentoriCtx } from './SentoriProvider.js'
|
|
4
|
+
|
|
5
|
+
import type { CaptureExtras, SentoriContextValue } from './types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Imperative access to the SDK from inside a component. Returns the
|
|
9
|
+
* full context — most code only needs `captureError`, `addBreadcrumb`,
|
|
10
|
+
* or `setUser`.
|
|
11
|
+
*
|
|
12
|
+
* const { captureError, addBreadcrumb } = useSentori()
|
|
13
|
+
* try {
|
|
14
|
+
* await api.checkout(order)
|
|
15
|
+
* } catch (e) {
|
|
16
|
+
* captureError(e as Error, { tags: { stage: 'checkout' } })
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
export function useSentori(): SentoriContextValue {
|
|
20
|
+
return useSentoriCtx()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wraps an async function so any thrown / rejected error is captured
|
|
25
|
+
* and rethrown. The wrapper is `useCallback`-stable across renders if
|
|
26
|
+
* `extras` is stable too — pass it inside `useMemo` if you build it
|
|
27
|
+
* inline.
|
|
28
|
+
*
|
|
29
|
+
* const checkout = useCaptureError(
|
|
30
|
+
* async (order: Order) => api.checkout(order),
|
|
31
|
+
* { tags: { stage: 'checkout' } },
|
|
32
|
+
* )
|
|
33
|
+
* await checkout(order) // captures + throws on failure
|
|
34
|
+
*/
|
|
35
|
+
export function useCaptureError<TArgs extends unknown[], TRet>(
|
|
36
|
+
fn: (...args: TArgs) => Promise<TRet> | TRet,
|
|
37
|
+
extras?: CaptureExtras,
|
|
38
|
+
): (...args: TArgs) => Promise<TRet> {
|
|
39
|
+
const { captureError } = useSentoriCtx()
|
|
40
|
+
return useCallback(
|
|
41
|
+
async (...args: TArgs) => {
|
|
42
|
+
try {
|
|
43
|
+
return await fn(...args)
|
|
44
|
+
} catch (e) {
|
|
45
|
+
captureError(e instanceof Error ? e : new Error(String(e)), extras)
|
|
46
|
+
throw e
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
[captureError, fn, extras],
|
|
50
|
+
)
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { SentoriProvider } from './SentoriProvider.js'
|
|
2
|
+
export { SentoriErrorBoundary } from './SentoriErrorBoundary.js'
|
|
3
|
+
export { useSentori, useCaptureError } from './hooks.js'
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
Breadcrumb,
|
|
7
|
+
BreadcrumbType,
|
|
8
|
+
CaptureExtras,
|
|
9
|
+
SentoriContextValue,
|
|
10
|
+
SentoriReactConfig,
|
|
11
|
+
Tags,
|
|
12
|
+
User,
|
|
13
|
+
} from './types.js'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Breadcrumb,
|
|
3
|
+
BreadcrumbType,
|
|
4
|
+
CaptureExtras,
|
|
5
|
+
CommonInitOptions,
|
|
6
|
+
Tags,
|
|
7
|
+
User,
|
|
8
|
+
} from '@goliapkg/sentori-core'
|
|
9
|
+
|
|
10
|
+
export type SentoriReactConfig = CommonInitOptions & {
|
|
11
|
+
/**
|
|
12
|
+
* Disable the JS SDK's automatic window/process error hooks. Default
|
|
13
|
+
* is to leave them on so non-React errors (network handlers, top-
|
|
14
|
+
* level promises) still capture. Set to false if you want
|
|
15
|
+
* SentoriErrorBoundary to be the only entry point.
|
|
16
|
+
*/
|
|
17
|
+
enableGlobalHooks?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SentoriContextValue = {
|
|
21
|
+
/** Append a breadcrumb to the per-process ring buffer. */
|
|
22
|
+
addBreadcrumb: (type: BreadcrumbType, data?: Record<string, unknown>) => void
|
|
23
|
+
/**
|
|
24
|
+
* Capture any thrown value. Plain `Error` is the happy path; non-
|
|
25
|
+
* Error values get wrapped so the dashboard still sees a stack.
|
|
26
|
+
*/
|
|
27
|
+
captureError: (error: Error, extras?: CaptureExtras) => void
|
|
28
|
+
/** Same as captureError. Kept for parity with the JS SDK. */
|
|
29
|
+
captureException: (error: Error, extras?: CaptureExtras) => void
|
|
30
|
+
/** Whether SentoriProvider has finished its one-shot init. */
|
|
31
|
+
initialised: boolean
|
|
32
|
+
/** Attach a stable user identifier to subsequent events. */
|
|
33
|
+
setUser: (user: null | User) => void
|
|
34
|
+
/** Replace the entire tag set on subsequent events. */
|
|
35
|
+
setTags: (tags: Tags | null) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Re-exports so consumers don't have to depend on @goliapkg/sentori-core
|
|
39
|
+
// directly to type-annotate the props they pass in.
|
|
40
|
+
export type { Breadcrumb, BreadcrumbType, CaptureExtras, Tags, User }
|